# Mario Tennis Fever (`0400D4201C6EA000`) A Switch 2 game: update v327680 (1.1.1) over the base game, SDK 20.5.17 built for the Switch 2 (`Ounce`), NVN2. Camelot's engine (`nxe`) on NintendoWare (G3d, Ui2d, Vfx, Atk), with Enlighten, Pia and NPLN. Modules `main` and `sdk`, plus one NRO that `main` decodes at run time. **Plays on the Mac** (M4 Pro): the title movie, the practice, the main menu, Adventure's opening story, and Free Play singles on Stadium Court (Grass) with real rallies, at 60 fps. No IPA has been built. ## What it needs | Setting | Where | Why | |---|---|---| | `mem_alias 0x500000000`, `mem_reserve 0x650000000` | [`game.tsv`](game.tsv) | `nninitStartup` asks `StandardAllocator` for 16 GB of address space, and this SDK keeps its 1 GB node area (no node-area cut matches it): with a 16 GB alias region `CentralHeap::Init` aborted (`sdk+0x13f698`) | | `mem_stack 0x10000000`, `app_pool 0x240000000` | `game.tsv` | Splatoon Raiders' values, the Switch 2's 9 GB, not measured here | | `sys_resource 0x2000000` | `game.tsv` | the NPDM declares 32 MB, so virtual address memory is on | | `pad_layout full` | `game.tsv` | the game asks for most of the controller | Render conditions, which the host evaluates for every game ([Conditional rendering evaluated at replay](../../../docs/reference/rendering.md#conditional-rendering-evaluated-at-replay); until 2026-11-12 the profile key `render_cond`): as the console does. Off, a rally looks the same and takes about 0.7 of 9.5 ms more GPU (one run each, with another job on the Mac) and logs a red line. The NRO and the archive need no key: the NSP script captures one and `shader_split.py` reads the other (below). ## Files - A dump folder (`base/`, `updates/327680/`): `build_ipa_from_nsp.py --platform mac` packs it with no keys. The display version `v327680` is a placeholder, as a dump has no control data. - `main` has about 600 KB of code. It is a boot stub (`nxboot`): six threads unscramble and unpack the game's code, a 24.7 MB NRO, out of its data segment, and it loads that with `nn::ro` ([A game that decodes its code at run time](../../../docs/reference/services.md#a-game-that-decodes-its-code-at-run-time)). The NSP script captures it from a boot in about 2 s. Its SHA-256 is the one the RomFS's NRR registers ([NRR version 1](../../../docs/reference/services.md#nrr-version-1)). The stub obfuscates its strings and streams, but runs as it is: no game code is changed for this. - The RomFS is `42` (7.6 GB, magic `CRA.`) and the NRR. `42` holds 8,671 files, listed only in the NRO: 5,474 sound, 164 WebM movies, the rest NARC containers of FRES, FSHA, BNTX, VFXB, SARC ([One scrambled archive indexed by the NRO](../../../docs/reference/shaders.md#one-scrambled-archive-indexed-by-the-nro)). `dm/gfx/tools/cra.py <42> ` writes them out. - Movies: VP9 WebM with Vorbis, up to 2560x1440 at 60 fps; the title movie is 368 MB. The game reads them itself from the archive and feeds the SDK's extractor (`AppendMediaSourceData`). ## Shaders 35,045 in [`shader_map.tsv`](shader_map.tsv): 26,096 from models (one FSHA each), 5,331 from `ssg_system`, 3,316 from effects, 138 from `shader/`, 124 from layouts, 40 in the NRO's data. All translate. A full build takes about an hour on an M4 Pro with another job running (not timed from an empty cache in one go). [`pso_manifest.tsv`](pso_manifest.tsv) (1,351 lines) is one Mac session: boot, the movie, the menus, a match. One does not load (current): `3032440e5027bbe5`, `shader/audience/audience_compute`, reads `array, 32> cp_t2d_cb0_0`, the compute texture handles of NVN2's per-shader `c[0]` indexed by its workgroup ID. `dmsh_reflect.py` `c2_array` only knows c2; fixing it is a chain change. The dispatch is skipped, a red line says so, and the crowd still draws; what the shader adds has not been seen. ## Rendering - The scene renders at 2048x1152 into a 2560x1440 window texture, and the game crops the window to that (`nvnWindowSetCrop`). `DM_NVN_SHOT` takes the whole texture, so shots have a black band on the right and bottom; the window does not. - Deferred: albedo and normals, then lighting into R11G11B10F, the atmosphere over it, bloom, an ACES tonemap. The atmosphere's table is a 3D texture of 8x8x64, filled once per stage with one draw per plane through a geometry shader that picks the layer ([A passthrough geometry shader that writes the layer](../../../docs/reference/shaders.md#a-passthrough-geometry-shader-that-writes-the-layer): the blue wash). - Frame time in a rally, in a window of 300 frames, with another session's regression on the Mac: p50 16.6–17.2 ms, p95 about 18 ms, GPU p50 about 9 ms, p95 about 11 ms. A few single frames take 90–140 ms, not traced (open); a run after the prewarm list has not been compared. ## Driving it on the Mac The work folder `0400D4201C6EA000-work` beside the dump has `run/boot.sh [VAR=value…]`, `run/drive.py` (control lines, `wait S`, `snap NAME`), `run/tomatch.py ` (from a save, to the first serve of a Free Play match in about 100 s) and `run/rally.py`. With no save, A from 25 s goes through the movie into the practice, and Plus twice leaves it for the main menu. The stick moves the menus (`LDown`). A tosses and hits. The opponent and court are the menu's defaults or random. ## Open - **Tessellated grass is not drawn.** `shader/grass` and `shader/sea_of_clouds` use hull and domain shaders (two each), which the host has no path for: `primitive type 14 not implemented` and `geometry draw cannot capture this pipeline` (tessellation with a geometry shader), and those draws are skipped. The court's own texture and stripes draw, so grass courts look flat; compared with nothing yet. - `3032440e5027bbe5` above. - Nothing past the first Adventure scenes and one Free Play match has been played: no other court, doubles, the online modes (no network on the host). - Not checked: game speed against the console; the host's `IsRunningOnOunce` answers false. - `linear texture storage is not in a memory pool (1024x1024 format 0x26)`, once in some runs, no effect seen. - An IPA: the 20 GB alias region is over iOS's limit until this SDK gets a node-area cut ([An SDK 21 Switch 2 game with a 4.5 GB alias region](../../../docs/reference/memory.md#an-sdk-21-switch-2-game-with-a-45-gb-alias-region)). - [Stack slots reused after `ro::Initialize`](../../../docs/reference/threads.md#a-threads-arguments-in-slots-its-starter-reuses-after-connecting-to-a-service) are held off by a modelled time, not an order. ## Found on this game - [A game that decodes its code at run time](../../../docs/reference/services.md#a-game-that-decodes-its-code-at-run-time) and [NRR version 1](../../../docs/reference/services.md#nrr-version-1). - [One scrambled archive indexed by the NRO](../../../docs/reference/shaders.md#one-scrambled-archive-indexed-by-the-nro). - [A shared font larger than the game's font engine memory](../../../docs/reference/services.md#a-shared-font-larger-than-the-games-font-engine-memory): a null pointer in `nn::font` at boot. - [A thread's arguments in slots its starter reuses after connecting to a service](../../../docs/reference/threads.md#a-threads-arguments-in-slots-its-starter-reuses-after-connecting-to-a-service): a crash at boot about once in a dozen runs. - [The submission-ready event counted as packets](../../../docs/reference/movies.md#the-submission-ready-event-counted-as-packets): black movies, then corrupt frames. - [A passthrough geometry shader that writes the layer](../../../docs/reference/shaders.md#a-passthrough-geometry-shader-that-writes-the-layer): a blue wash over every 3D scene with atmosphere. - [Null vertex buffers bound past stream 15](../../../docs/reference/rendering.md#null-vertex-buffers-bound-past-stream-15).