# 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-wide rules, `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). - **Resolved: texture reads removed by the translator** (GitHub issue #168). 58,122 reads, 57,861 of them texture-buffer reads in Unreal's Niagara particle vertex factories, reached their handle through a phi that also carried non-handle values, and bindless elimination replaced them with 0: particle scale 0, so fire, smoke and muzzle-flash meshes collapsed to a point. 58,096 of them in 3,197 shaders are now resolved; 26 in 14 shaders, whose handle comes from no c2 word, are still removed. The SPIR-V of the 69,449 other shaders is unchanged. See [Bindless reads whose handle phi also carries other values](../../../docs/reference/shaders.md#bindless-reads-whose-handle-phi-also-carries-other-values). - **Current: particle data in buffer textures at offsets 4, 8 and 12 past a 16-byte boundary** (pool offset `0x2fd208`, for one), which an Apple GPU reads from the boundary below. They are read through an aligned copy ([Buffer textures at offsets below 16 bytes](../../../docs/reference/rendering.md#buffer-textures-at-offsets-below-16-bytes)). - **Unbuilt:** 11 shaders, none met so far: 3 geometry shaders and 8 compute shaders with 3D image atomics. Image atomics are native texture atomics since chain `0ce4b5e8aefb`, which build 3D ones on Octopath Traveler 0 and Ghost Trick ([Image atomics](../../../docs/reference/shaders.md#image-atomics)); this game's library has not been rebuilt on it yet. ## 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. - **Checked 2026-10-28:** the [IoStore patch-container resolver](../../../docs/reference/shaders.md#iostore-patch-containers-keep-their-shader-groups-in-the-base-container) preserves this game's single-container extraction on update v65536. Both decoded archives match the original reader byte for byte: 3,542 + 70,702 shader entries, 73,496 distinct control-and-code keys, 707,636,257 archive bytes; both readers together took 39.4 s through the existing Mac host's Oodle service. This checks extraction only; gameplay was not rerun for this change. The full `regress.py check` was not completed (cold shader rebuild). - 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`): [Unreal's FMallocBinned3 replaced by the host allocator](../../../docs/reference/memory.md#unreals-fmallocbinned3-replaced-by-the-host-allocator). - **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), [Unreal Engine 5.6 IoStore shader libraries](../../../docs/reference/shaders.md#unreal-engine-56-iostore-shader-libraries). - **Rewriter:** text before the first FDE as one function (`func_of`) and long functions analysed in pieces (`pieces`): [Code before the first unwind entry is one function](../../../docs/reference/rewriter.md#code-before-the-first-unwind-entry-is-one-function), [Long functions analysed in pieces](../../../docs/reference/rewriter.md#long-functions-analysed-in-pieces). - **Bindless reads through a phi that also carries non-handle values** (the vendored translator's `TryTableHandle`): [Bindless reads whose handle phi also carries other values](../../../docs/reference/shaders.md#bindless-reads-whose-handle-phi-also-carries-other-values). - **Buffer texture views below the 16-byte alignment read through an aligned copy** (`tbuf_shadow`): [Buffer textures at offsets below 16 bytes](../../../docs/reference/rendering.md#buffer-textures-at-offsets-below-16-bytes). ## 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). The host now binds the zero 3D texture there ([A 2D texture in a 3D slot binds the zero 3D texture](../../../docs/reference/rendering.md#a-2d-texture-in-a-3d-slot-binds-the-zero-3d-texture)); not run on this game since. - 11 shaders do not build (3 geometry, 8 compute with 3D image atomics); none met so far. - **Resolved: the image-atomic guards of `c3a0880` (2026-10-25) broke the opening.** 842 more compute shaders did not translate (`MSL 2.3 image atomics require IGN with a discarded result`; `41b20b5f09269ad8` is dispatched in the opening), and a new game aborted the host at the opening cutscene (`FATAL [RED] image atomics: unsupported or overflowing IMG extent`, compute shader `f3c5ced35cb619d0`, image slot 2, texture ID 2142). Since `2e655b7` translates image atomics whose result is used, chain `39e4d9585b1d` builds all but the 8 listed above, and a new game from an empty save plays the opening cutscene: both shaders run their atomics directly on their buffer textures (target 10, 8 KB). Checked on the Mac on 2026-10-26.