From 6cd03fb630cddfa20a2b2b3d8d42f8e909e8f533 Mon Sep 17 00:00:00 2001 From: chrg6 Date: Wed, 11 Nov 2026 00:14:44 -0500 Subject: [PATCH] Declare a texture whose size query reads its layer count as an array, so the count is the texture's own. A program that only asks a combined-handle texture for its size got that texture declared as plain 2D, the runtime bound a one-layer view, and the layer count read as 1 where the console answers the texture's real count. The game's particle vertex shaders read exactly that count, of a texture their pixel shaders sample as a 2D array, and use it as the frame count of a flipbook, so such particles stayed on their first frame. A size query that reads the component after a 1D or 2D type's dimensions now makes the declaration an array; a plain texture bound through an array view still reports one layer. 74 of the stored shader variants change (the contributor's sweep, all valid; a reader here found the same 74 in a shader cache on this PC), and the contributor's before-and-after pictures differ in the one scene tried, the Wallmaster's warning disc. Goes in with the image read capability so that installs translate their shaders again once. Built here; tools/xlate_smoke.py's new case reports the texture as plain 2D with the translator before the patch and as a 2D array with it, and all three modules validate. Not played with the new translator. Co-Authored-By: Claude Fable 5.2 (cherry picked from commit e4fac75190b4baf3811feef45b3302a9216cb8b2) --- tools/build_xlate.py | 4 +-- ...e-query-past-the-type-reads-an-array.patch | 24 ++++++++++++++ tools/xlate_smoke.py | 32 +++++++++++++++---- 3 files changed, 51 insertions(+), 9 deletions(-) create mode 100644 tools/xlate/patches/0040-size-query-past-the-type-reads-an-array.patch diff --git a/tools/build_xlate.py b/tools/build_xlate.py index 3c755609..ef52f6d4 100644 --- a/tools/build_xlate.py +++ b/tools/build_xlate.py @@ -99,8 +99,8 @@ def prepare_sources() -> Path: for junk in list(src.glob("src/*/obj")) + list(src.glob("src/*/bin")): shutil.rmtree(junk) patches = sorted((XLATE / "patches").glob("*.patch")) - if len(patches) != 38: - sys.exit(f"expected 38 patches, found {len(patches)}") + if len(patches) != 39: + sys.exit(f"expected 39 patches, found {len(patches)}") # build/xlate is inside this checkout, and git inside a repository reads a "diff --git" patch's paths from the # repository's top: those hunks fall outside the directory handed to --directory, git skips every file of the patch # and still exits 0. GIT_CEILING_DIRECTORIES stops git from walking up to the checkout, so it reads each patch as it diff --git a/tools/xlate/patches/0040-size-query-past-the-type-reads-an-array.patch b/tools/xlate/patches/0040-size-query-past-the-type-reads-an-array.patch new file mode 100644 index 00000000..d958d8e4 --- /dev/null +++ b/tools/xlate/patches/0040-size-query-past-the-type-reads-an-array.patch @@ -0,0 +1,24 @@ +--- a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitTexture.cs ++++ b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitTexture.cs +@@ -1116,6 +1116,21 @@ + else + { + type = context.TranslatorContext.GpuAccessor.QuerySamplerType(imm); ++ ++ // NX2NAVI: a size query that reads the component after a 1D or 2D type's dimensions asks for the layer ++ // count, so the texture is an array. That holds for any 1D or 2D type here: the accessor's answer, or the 2D ++ // it answers for a handle it has no type for. The game's particle vertex shaders read only the layer count ++ // of such a handle, which their pixel shaders sample as a 2D array, and use it as the frame count of a ++ // flipbook; declared 2D, the binding is a one-layer view and the count reads as 1. A plain texture bound ++ // through an array view still reports one layer. Not handled: a program that samples the same handle as a ++ // plain 2D texture before this query gets an arrayed declaration for that sample (invalid SPIR-V). ++ if (query == TexQuery.TexHeaderDimension && ++ ((type & SamplerType.Mask) == SamplerType.Texture1D || (type & SamplerType.Mask) == SamplerType.Texture2D) && ++ !type.HasFlag(SamplerType.Array) && ++ (componentMask & (1 << type.Dimensions)) != 0) ++ { ++ type |= SamplerType.Array; ++ } + } + + TextureFlags flags = isBindless ? TextureFlags.Bindless : TextureFlags.None; diff --git a/tools/xlate_smoke.py b/tools/xlate_smoke.py index 0a407f35..1b22f707 100644 --- a/tools/xlate_smoke.py +++ b/tools/xlate_smoke.py @@ -1,10 +1,11 @@ """Smoke test for build/xlate/nx2navi_xlate(.exe): no game files needed. Sends one hand-assembled Maxwell fragment shader (writes the constant colour 1.0, 0.5, 0.25, 1.0 to render target 0) -through --serve, checks the reply, then sends a malformed line and checks the service survives and still answers. A -hand-assembled compute shader that loads a texel from a float image and stores it back follows: the translator -declares such an image without a format, and its read needs StorageImageReadWithoutFormat. Every module is validated -with spirv-val. +through --serve, checks the reply, then sends a malformed line and checks the service survives and still answers. Two +hand-assembled compute shaders follow. One loads a texel from a float image and stores it back: the translator +declares such an image without a format, and its read needs StorageImageReadWithoutFormat. The other reads only the +layer count of texture handle 0x10, for which the translator has no type answer: the texture must be declared as an +array, or the count reads as 1. Every module is validated with spirv-val. python tools/xlate_smoke.py """ @@ -47,6 +48,7 @@ def fragment_code() -> bytes: SULD_P, SUST_P = 0x1D61, 0x1D65 # opcode bits 51-63 of SULD.P and SUST.P +TXQ = 0x1BE9 # opcode bits 51-63 of TXQ (bound handle) def surface_p(opcode: int, data: int, coord: int, handle: int) -> int: @@ -61,6 +63,14 @@ def image_read_code() -> bytes: return struct.pack(f"<{len(words)}Q", *words) +def layer_count_code() -> bytes: + """Compute code: R0 = the layer count (component 2 of the dimension query) of texture handle 0x10, stored to image 0.""" + txq = (TXQ << 51) | (0x10 << 36) | (0x4 << 31) | (1 << 22) | PT | (RZ << 8) | 0 # TexHeaderDimension, mask z, LOD RZ + sust = (SUST_P << 51) | (IMAGE0 << 36) | (0x1 << 20) | PT | (RZ << 8) | 0 # SUST.P 1D R of R0 at x 0 + words = [SCHED, txq, sust, EXIT] + return struct.pack(f"<{len(words)}Q", *words) + + def main() -> int: if not EXE.is_file(): print(f"missing {EXE}; run tools/build_xlate.py", file=sys.stderr) @@ -71,24 +81,32 @@ def main() -> int: code.write_bytes(fragment_code()) image_code = tmp / "cs.bin" image_code.write_bytes(image_read_code()) + layer_code = tmp / "layers.bin" + layer_code.write_bytes(layer_count_code()) out = tmp / "out" job = {"key": "const_ps", "stage": "ps", "code": str(code), "debug": False, "fout": {"0": "float"}} image_job = {"key": "image_read_cs", "stage": "cs", "code": str(image_code), "debug": False, "local": [1, 1, 1]} + layer_job = {"key": "layer_count_cs", "stage": "cs", "code": str(layer_code), "debug": False, "local": [1, 1, 1]} lines = [ json.dumps({"job": job, "out": str(out)}), "this is not json", json.dumps({"job": dict(job, key="again"), "out": str(out)}), json.dumps({"job": image_job, "out": str(out)}), + json.dumps({"job": layer_job, "out": str(out)}), ] r = subprocess.run([str(EXE), "--serve"], input="\n".join(lines) + "\n", capture_output=True, text=True, timeout=60) replies = r.stdout.splitlines() print("replies:", replies) - if (len(replies) != 4 or replies[0] != "ok const_ps" or not replies[1].startswith("fail ") or replies[2] != "ok again" - or replies[3] != "ok image_read_cs"): + if (len(replies) != 5 or replies[0] != "ok const_ps" or not replies[1].startswith("fail ") or replies[2] != "ok again" + or replies[3] != "ok image_read_cs" or replies[4] != "ok layer_count_cs"): print("unexpected replies; stderr:", r.stderr, file=sys.stderr) return 1 failed = 0 - for key in ("const_ps", "image_read_cs"): + layer_types = [t["type"] for t in json.loads((out / "layer_count_cs.res.json").read_text())["textures"]] + if layer_types != [0x103]: # SamplerType Texture2D (3) | Array (0x100) + print(f"layer_count_cs: texture types {layer_types}, expected a 2D array [259]", file=sys.stderr) + failed = 1 + for key in ("const_ps", "image_read_cs", "layer_count_cs"): spv = out / f"{key}.spv" data = spv.read_bytes() if len(data) < 20 or struct.unpack("