Files
nxnative-easy/dm/games/0100606026F12000/README.md
T
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

77 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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).
- **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 `<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:
- **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).
- 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.