Files
seedds f8642c480f Run image atomics as native Metal texture atomics, 3D images and CAS included
Below MSL 3.1, SPIRV-Cross emulates an image atomic with a raw device pointer
beside the texture, laid out like a linear image. A block-linear image's Metal
texture is a deswizzled copy, so its atomics went to a scratch buffer nothing
read (Pokemon Legends: Z-A's atomic max into texture 302), and a 3D image's
pointer was indexed with the int3 coordinate itself, which does not compile
(16 shaders of Octopath Traveler 0, 5 of Ghost Trick, 8 of FINAL FANTASY
RESONANCE).

- dmsh_atomic.py: shaders with image atomics (OpImageTexelPointer) are written
  at MSL 3.1, so the atomics run on the image's own texture; every other
  shader keeps its MSL 2.3 source byte for byte. MSL that still holds the
  pointer form fails the build.
- Translator (TranslationFlags.MetalImageAtomics, renamed from
  Msl23ImageAtomics): admits 3D images and SUATOM.CAS (Metal's texture
  compare-exchange); the IGN coordinate guard is unchanged.
- Host: the IMGA pointer binding, the scratch buffer and the layout admission
  are gone. NVN images of R32Uint / R32Sint take MTLTextureUsageShaderAtomic
  (img_usage, kept by tex_upgrade: Metal leaves that bit out of the usage
  property); any other texture in an atomic slot is replaced by an
  atomic-capable write sink with a red line.
- Chain a4e2088c2e69 (with main's vertex-output padding). Tested on
  chain 0ce4b5e8aefb, this change before that rebase: Octopath Traveler 0
  builds all 10,324 shaders, Z-A all
  28,603, Ghost Trick's five 3D-atomic shaders build. Mac runs of both games
  under the Metal API validation layer showed no validation error (Octopath
  Traveler 0 to frame 14,400, its particle atomics on buffer texture 2049;
  Z-A to frame 12,000 through the atomic max on texture 302). Synthetic
  atomics and CAS run on buffer, 2D, 2D-array and 3D textures of a Mac GPU
  (test_image_atomic_bounds.py, t_atomic_return.m).

GitHub issue #290: at 40b643b Octopath Traveler 0 already built 10,308 of
10,324 shaders (2e655b7 fixed the returning atomics); the 16 left were the 3D
ones, which build now. Not checked: an iPhone, FINAL FANTASY RESONANCE's
library, the picture of Z-A's atomic scene.
2026-10-27 03:01:45 +08:00

14 KiB
Raw Permalink Blame History

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). 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 cut: main+0x137f0 bl 0xae770 → nop 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
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
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 (unless -SwitchVirtualAddressSpaceSize= caps it, main+0x137d0), 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; 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).
  • 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.
  • 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

  • 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.
  • 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.