Two general causes of FINAL FANTASY RESONANCE's missing fires, smoke and muzzle flashes, found and first fixed by b1tdr3amer24 (GitHub issue #168). - Translator (vendored BindlessElimination.cs, TryTableHandle): a texture read whose handle phi mixes c2 handle words with values that are not handles (undefined, or float values the compiler kept in the register on paths where the predicated read does not run) was removed and its result set to 0: 58,122 reads in FINAL FANTASY RESONANCE, mostly Niagara particle vertex factories, whose meshes collapsed to a point. The read now takes the c2 handle among the inputs, or reads each candidate and picks by the phi's run- time value; candidates read elsewhere as another texture shape are left out (a second sweep, ResourceManager.GetTextureTypes). Only phis that GenerateBindlessAccess refuses come here, so tex_dyn_slots arrays keep their path: the other 69,449 shaders' SPIR-V is unchanged. Several candidates are never used for an image store or atomic. dmsh_xlate's Log now reaches the notes as "translator: ..." and the build summary counts those apart. - Host (nvn_mtl.m, tbuf_shadow): a texture buffer view at an offset that is not 16-byte aligned reads from the boundary below on an M4 Pro (t_tbuf_align: 56 of 56 views wrong; the M6 measurement in the old comment does not hold). Such views are read through a per-submission aligned copy; aligned views are untouched. A workaround for the Metal limit, its gaps (GPU writes in the same submission, CPU_NO_ACCESS pools, image writes) recorded as open. Translator rebuilt; lock and chain header updated. Checked: all FFR shaders translated before and after; FFR on the Mac (fires and smoke column render, steady; with the copy off they flicker); gfx, chain and docs tests. Co-authored-by: b1tdr3amer24 <240231594+b1tdr3amer24@users.noreply.github.com>
4.5 KiB
Ryujinx shader translator (vendored subset)
Three projects from Ryujinx 1.3.2: Ryujinx.Graphics.Shader, Ryujinx.Common and Spv.Generator, with these local changes:
-
Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs,GenerateTextureSample:- Texel offsets that are not constants (Maxwell AOFFI / TLD4.PTP offsets read from a register) are built with
OpCompositeConstructand passed asOffset. A four-offset gather becomes four one-offset gathers, each giving component w. Upstream wrote them as anOpConstantCompositewithConstOffset(s), which is invalid SPIR-V; SPIRV-Cross threwnullptrand the shader never built (2026-10-18: 8 shaders in Wonder, Link's Awakening, Tears of the Kingdom and Age of Calamity). Markednxnative:in the code. - Texture coordinates that are NaN are replaced by 0 (the
nfZeroselect). This came with the first import.
- Texel offsets that are not constants (Maxwell AOFFI / TLD4.PTP offsets read from a register) are built with
-
Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessElimination.cs,TryGetOperation/IsSameOperand: a bindless handle that reaches the texture instruction through a phi is followed through the phis that feed it (a handle kept in a loop variable comes back through the loop's own phi), and twoconstant buffer | constanthandles count as the same value. Upstream compared only the phi's direct inputs, so such handles fell to the texture-pool array of 32 (*_t2d_cb0_40000000), which cannot be bound; 218 pixel shaders in Monster Hunter Rise (2026-10-20). Markednxnative:in the code. -
The same file,
TryTableHandleandIsBindlessHandleAllowed, withTranslation/Optimizations/Optimizer.cs(a second sweep) andTranslation/ResourceManager.cs(GetTextureTypes): a handle phi thatGenerateBindlessAccessrefuses, because some inputs are not handles (undefined, or values kept in the same register on paths where the predicated read does not run), takes the c2 handles among its inputs. With one, it takes the phi's place; with several, one read per handle, and the result is chosen by comparing the phi's value with each handle at run time. Upstream removed such reads and wrote 0: 58,122 reads in FINAL FANTASY RESONANCE (2026-10-26, GitHub issue #168). See Bindless reads whose handle phi also carries other values. Markednxnative:in the code. -
Ryujinx.Graphics.Shader/Instructions/InstEmitTexture.cs,EmitTex(TLD 1D with integer coordinates only) andEmitTexs(TLDSTexture1DLodZero), withIGpuAccessor.RecordTexelFetch1D: Maxwell reads a 1D texture and a texture buffer with the same instruction, and the bound descriptor decides. When the texture type query does not answer "buffer", the read is translated as 2D at(x & 0x3FFF, x >> 14)instead of(x, 0), and the handle is reported. A 1D texture (one row at most 16384 wide on the host) reads the same; the host binds a texture buffer in such a slot as a linear 2D texture of 16384-texel rows over its memory (nvn_mtl.mtbuf_as_2d). Upstream read(x, 0)and the host had only a dummy texture to give: the light and decal lists of Age of Calamity (at least 15,000 shaders) and Three Houses read zeros (2026-10-21). Markednxnative:in the code. -
Ryujinx.Graphics.Shader/Translation/EmitterContext.csandTranslation/ShaderDefinitions.cs: fixed-input compute lowering explicitly forwards passthrough positions and all captured generic attributes not replaced by the geometry shader, and inherits its output primitive from input topology. Independent offline stages have no next-stage usage mask, and Metal cannot provide the native NV passthrough raster stage. See passthrough compute emission and output bounds. -
Ryujinx.Graphics.Shader/Instructions/InstEmitSurface.cs,IntermediateRepresentation/TextureFlags.csandCodeGen/Spirv/Instructions.cs: the MSL 2.3 path bounds image atomics before forming the texel pointer. ReturningIGNatomics preserve the old texel on the valid edge and return zero outside the image; CAS, predicate results and non-IGN modes remain rejected. See bounded MSL 2.3 atomic emulation, found on SHIN MEGAMI TENSEI V: Vengeance's particle counters.
The shader translation tool in dm/gfx/tools/dmsh_xlate references them to translate the original Switch shaders to SPIR-V.
License: MIT, see LICENSE.txt. Upstream: https://github.com/Ryubing.