From 03e31c014be719b9b30810d9c4e98fbe47fadc8e Mon Sep 17 00:00:00 2001 From: chrg6 Date: Sat, 7 Nov 2026 02:26:10 -0500 Subject: [PATCH] Resolve storage buffers whose address a shader reads from a constant buffer at a run-time offset. Patch 0028: when GlobalToStorage finds no base for a global access whose address is a constant buffer load at a non-constant offset, every table entry that offset can reach becomes a candidate of the multi-target helper ({u64 address, u32 size} entries 16 bytes apart: C to C + 240 for E + C, each constant for phi(constants) << 4), and the helper uses the one whose range holds the address. Ocarina's NVN2 compute shaders pick c0 storage slots this way; unresolved compute accesses fell from 445 in 21 shaders to 243 in 17 over a Kokiri boot. Co-Authored-By: Claude Opus 5.5 --- tools/build_xlate.py | 4 +- ...28-storage-tables-at-dynamic-offsets.patch | 147 ++++++++++++++++++ 2 files changed, 149 insertions(+), 2 deletions(-) create mode 100644 tools/xlate/patches/0028-storage-tables-at-dynamic-offsets.patch diff --git a/tools/build_xlate.py b/tools/build_xlate.py index e34ca46f..f4577ed5 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) != 26: - sys.exit(f"expected 26 patches, found {len(patches)}") + if len(patches) != 27: + sys.exit(f"expected 27 patches, found {len(patches)}") for patch in patches: # git apply works outside a repository when given a directory; --check first so a failure names the patch. for extra in (["--check"], []): diff --git a/tools/xlate/patches/0028-storage-tables-at-dynamic-offsets.patch b/tools/xlate/patches/0028-storage-tables-at-dynamic-offsets.patch new file mode 100644 index 00000000..3d1f7f6b --- /dev/null +++ b/tools/xlate/patches/0028-storage-tables-at-dynamic-offsets.patch @@ -0,0 +1,147 @@ +--- a/src/Ryujinx.Graphics.Shader/Translation/ResourceManager.cs ++++ b/src/Ryujinx.Graphics.Shader/Translation/ResourceManager.cs +@@ -145,6 +145,20 @@ + return Properties.AddLocalMemory(new MemoryDefinition(name, type, arrayLength)); + } + ++ // NX2NAVI: the constant buffer slot that was given this binding, -1 when none was (no binding is created). ++ public int GetConstantBufferSlotOfBinding(int binding) ++ { ++ for (int slot = 0; slot < _cbSlotToBindingMap.Length; slot++) ++ { ++ if (_cbSlotToBindingMap[slot].Binding == binding) ++ { ++ return slot; ++ } ++ } ++ ++ return -1; ++ } ++ + public int GetConstantBufferBinding(int slot) + { + SetBindingPair setAndBinding = _cbSlotToBindingMap[slot]; +--- a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/GlobalToStorage.cs ++++ b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/GlobalToStorage.cs +@@ -709,6 +709,11 @@ + } + } + ++ if (targetCbs.Count == 0) ++ { ++ AddTableCandidates(resourceManager, operation.GetSource(0), targetCbs); ++ } ++ + targetCbs.Sort(); + + if (targetCbs.Count == 0) +@@ -862,6 +867,109 @@ + return "undef"; + } + ++ // NX2NAVI: a base read from a constant buffer at a run-time offset is an entry of a table of {u64 address, u32 ++ // size} descriptors (Ocarina's NVN2 compute picks a storage slot of c0's table by index: c0[x * 16 + 0xa0], ++ // c0[{5,6,7} << 4]). Every entry the offset can reach becomes a candidate of the multi-target helper, which uses ++ // the one whose range holds the address: E + C gives C, C + 16 ... C + 240; phi(constants) << 4 gives each. ++ private static void AddTableCandidates(ResourceManager resourceManager, Operand address, List targetCbs) ++ { ++ Operation load = FindDynamicCbLoad(address, 4); ++ ++ if (load == null || load.SourcesCount < 3 || load.GetSource(0).Type != OperandType.Constant) ++ { ++ return; ++ } ++ ++ int slot = resourceManager.GetConstantBufferSlotOfBinding(load.GetSource(0).Value); ++ ++ if (slot < 0) ++ { ++ return; ++ } ++ ++ // vecIndex = (E >> 2) >> 2 (EmitLoadConstant): E is the byte offset ++ Operand vec = load.GetSource(2); ++ ++ if (vec.AsgOp is not Operation sh1 || sh1.Inst != Instruction.ShiftRightU32 || ++ sh1.GetSource(0).AsgOp is not Operation sh2 || sh2.Inst != Instruction.ShiftRightU32) ++ { ++ return; ++ } ++ ++ Operand e = sh2.GetSource(0); ++ List offsets = []; ++ ++ if (e.AsgOp is Operation add && add.Inst == Instruction.Add) ++ { ++ Operand c = add.GetSource(1).Type == OperandType.Constant ? add.GetSource(1) : add.GetSource(0); ++ ++ if (c.Type == OperandType.Constant && (c.Value & 15) == 0) ++ { ++ for (int k = 0; k < 16; k++) ++ { ++ offsets.Add(c.Value + k * 16); ++ } ++ } ++ } ++ else if (e.AsgOp is Operation shl && shl.Inst == Instruction.ShiftLeft && ++ shl.GetSource(1).Type == OperandType.Constant && shl.GetSource(1).Value == 4 && ++ shl.GetSource(0).AsgOp is PhiNode phi) ++ { ++ for (int i = 0; i < phi.SourcesCount; i++) ++ { ++ if (phi.GetSource(i).Type == OperandType.Constant) ++ { ++ offsets.Add(phi.GetSource(i).Value * 16); ++ } ++ } ++ } ++ ++ foreach (int offset in offsets) ++ { ++ if (offset >= 0 && offset < 0x10000) ++ { ++ uint packed = PackCbSlotAndOffset(slot, offset); ++ ++ if (!targetCbs.Contains(packed)) ++ { ++ targetCbs.Add(packed); ++ } ++ } ++ } ++ } ++ ++ // The constant buffer load with a non-constant offset an address is built on (through Add and Copy), or null. ++ private static Operation FindDynamicCbLoad(Operand value, int depth) ++ { ++ if (depth < 0 || value.AsgOp is not Operation op) ++ { ++ return null; ++ } ++ ++ if (op.Inst == Instruction.Load && op.StorageKind == StorageKind.ConstantBuffer && op.SourcesCount >= 3 && ++ op.GetSource(2).Type != OperandType.Constant) ++ { ++ return op; ++ } ++ ++ if (op.Inst != Instruction.Add && op.Inst != Instruction.Copy) ++ { ++ return null; ++ } ++ ++ for (int i = 0; i < op.SourcesCount; i++) ++ { ++ Operation found = FindDynamicCbLoad(op.GetSource(i), depth - 1); ++ ++ if (found != null) ++ { ++ return found; ++ } ++ } ++ ++ return null; ++ } ++ + private static uint PackCbSlotAndOffset(int cbSlot, int cbOffset) + { + return (uint)((ushort)cbSlot | ((ushort)cbOffset << 16));