Skip to content
NX Native

FINAL FANTASY RESONANCE

60 fps with 1.0.1 Unreal Engine0100606026F12000

Plays on the iPhone; 60 fps (30 on the Switch) by the host's write of rhi.SyncInterval.

FINAL FANTASY RESONANCE with nxnative
Title ID
0100606026F12000
Tested build
1.0.1
Nintendo SDK
20.5.17
Engine
Unreal Engine 5.6.1
Graphics API
NVN

Build this game

Keep [0100606026F12000] in your file names, put the base game, update and DLC in one folder and run the script. Follow the build guide, then the sideloading guide. Needs an iPhone 15 Pro or newer, or an M1 iPad or later.

Technical notes

The project's own record of bringing this game up: what its profile needs and why, what was found and what is still open.

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). 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). Playable on an iPhone.

What it needs

Setting Where Why
fps_target 60 game.tsv, hle_fpslock.c the host writes 1 into rhi.SyncInterval; see Frame rate
FMallocBinned3 constructor call cut, main+0x137f0 bl 0xae770 → nop, with gate OBS_UE_MALLOC on it the rewriter's engine-wide rule (dm/tools/ue_malloc.py), no build ID the constructor reserves 38 GB of address space for its pools; the host runs Unreal's heap (hle_uemalloc.c, the UE 5.6 layout); see Memory. This build's own cut until 2026-10-30
Engine region given what the alias region has room for, 4.06 GB: gate OBS_UE_REGION on the store of its size, main+0x137cc the rewriter's engine-wide rule (dm/tools/ue_region.py), no build ID; the host's room (hle_svc.c) Unreal's nninitStartup reserves 62 GB by default. Until 2026-10-31 game_args -SwitchVirtualAddressSpaceSize=4064. 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
Integer output row c254eaa40e0c4f1e (fout 0 uint) 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 (main+0x137d0; the rewriter's gate on the size's store gives it what the alias region has room for, Unreal's engine address region and its options), then constructs its FMallocBinned3 in place in the bss, as Octopath Traveler 0's (01005270232F2000): 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 by the rewriter's engine-wide rule (dm/tools/ue_malloc.py), and at the gate on it the host puts its allocator's vtable in the object (hle_uemalloc.c; until 2026-10-30 before any guest code ran). 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 4.06 GB region (4064 MB from game_args until 2026-10-31). 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).
  • 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.
  • 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).
  • 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); 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 <library>-<format> 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 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 <addr> 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:

Open

  • The engine region's gate (4.06 GB instead of the 4064 MB game_args gave) has not been run on this game yet: the [mem] Unreal's engine region line in a boot's log shows it.
  • 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); 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.

These notes are republished from the nxnative repository under its MIT license (license and attribution). The original, which may be newer, is the game's notes on GitHub. This site hosts no game files.

More Unreal Engine games