From bf51ba33c3144843b75b68179f4f10c8e22a6fe5 Mon Sep 17 00:00:00 2001 From: seedds Date: Fri, 13 Nov 2026 17:59:57 +0100 Subject: [PATCH 1/3] shader: cache the resolved raw-global table entry in registers A raw global access (an address traced to no storage buffer) resolved its pool through the host's table on every access. The fast path kept only the last entry index, so a hit still loaded four map words to check the range and two more for the delta, all dependent on the index. Each helper kind now keeps the chosen entry's six words (start, end, delta) in a zero-initialised private array. A hit compares against those values and adds the cached delta without loading the table. A miss takes the hint or search as before and refills the cache. A vector access also checks that its last byte is inside the cached entry; the old last-index path skipped that check. Ocarina of Time (0400486024DEC000), M1 Pro / Honeykrisp G13, FSR1 tier 1, in-run mesh-set A/B (host DM_VK_MESH_B, 600-frame phases): - frame p50 32.75 -> 32.39 ms; p95 34.45 -> 34.15 ms; p99 36.18 -> 34.98 ms; - frames within 33.3 ms 73.5% -> 82.1%; - GPU -0.14 ms median, lower in all five adjacent pairs. Material-pass producers (offline shaderdb): static instructions 393k -> 428k, spills 329 -> 442, because cache refill code is added. The executed hit path is shorter. All 520 task/mesh modules pass spirv-val --target-env vulkan1.3. Screenshots in both arms render correctly. Co-Authored-By: Claude Opus 5.5 --- .../Optimizations/GlobalToStorage.cs | 88 ++++++++++++------- .../Translation/ResourceManager.cs | 7 ++ 2 files changed, 61 insertions(+), 34 deletions(-) diff --git a/dm/third_party/ryujinx/src/Ryujinx.Graphics.Shader/Translation/Optimizations/GlobalToStorage.cs b/dm/third_party/ryujinx/src/Ryujinx.Graphics.Shader/Translation/Optimizations/GlobalToStorage.cs index 9a72f2eb..193fee01 100644 --- a/dm/third_party/ryujinx/src/Ryujinx.Graphics.Shader/Translation/Optimizations/GlobalToStorage.cs +++ b/dm/third_party/ryujinx/src/Ryujinx.Graphics.Shader/Translation/Optimizations/GlobalToStorage.cs @@ -98,6 +98,21 @@ public int AddRawVectorFunction(int count, Function function) return functionId; } + private readonly Dictionary<(Instruction, StorageKind), int> _rawEntryCacheBindings = new(); + + public int GetOrAddRawEntryCacheBinding(ResourceManager resourceManager, Operation operation) + { + var key = (operation.Inst, operation.StorageKind); + + if (!_rawEntryCacheBindings.TryGetValue(key, out int binding)) + { + binding = resourceManager.AddRawEntryCacheMemory(); + _rawEntryCacheBindings.Add(key, binding); + } + + return binding; + } + public int GetOrAddRawLastBinding(ResourceManager resourceManager, Operation operation) { var key = (operation.Inst, operation.StorageKind); @@ -1006,17 +1021,36 @@ Operand Below(Operand high, Operand low) Operand lblFound = Label(); Operand lblChosen = Label(); - // Try the entry this helper chose last in this invocation before - // searching the table. Entry zero is empty, so the first lookup - // still searches. The host binds one disjoint table for the draw. - int lastBinding = gtsContext.GetOrAddRawLastBinding(resourceManager, operation); - Operand last = context.Load(StorageKind.LocalMemory, lastBinding); - Operand lastEntry = context.IMultiply(last, Const(GmemMapEntryWords)); - Operand lastHolds = context.BitwiseAnd( - AtOrBelow(MapWord(lastEntry, 1), MapWord(lastEntry, 0)), - Below(MapWord(lastEntry, 3), MapWord(lastEntry, 2))); - context.Copy(selected, last); - context.BranchIfTrue(lblChosen, lastHolds); + // The entry this helper resolved last in this invocation, kept as its start, end and delta words (ResourceManager.AddRawEntryCacheMemory): + // an address inside it, and for a vector its last byte too, takes it without loading the table at all. The cache starts zero (end 0), so the + // first lookup searches. The host binds one disjoint table for the draw. + int cacheBinding = gtsContext.GetOrAddRawEntryCacheBinding(resourceManager, operation); + + Operand CacheWord(int word) + { + return context.Load(StorageKind.LocalMemory, cacheBinding, Const(word)); + } + + Operand lastLow = addrLow; + Operand lastHigh = addrHigh; + + if (vectorCount > 1) + { + lastLow = context.IAdd(addrLow, Const(vectorCount * 4 - 1)); + lastHigh = context.IAdd(addrHigh, context.ConditionalSelect(context.ICompareLessUnsigned(lastLow, addrLow), Const(1), Const(0))); + } + + Operand LastBelow(Operand high, Operand low) + { + return context.BitwiseOr( + context.ICompareLessUnsigned(lastHigh, high), + context.BitwiseAnd(context.ICompareEqual(lastHigh, high), context.ICompareLessUnsigned(lastLow, low))); + } + + Operand cachedHolds = context.BitwiseAnd( + AtOrBelow(CacheWord(1), CacheWord(0)), + LastBelow(CacheWord(3), CacheWord(2))); + context.BranchIfTrue(lblChosen, cachedHolds); // A tagged 2 MiB-chunk hint names an entry which holds the whole // chunk. Collisions and ranges which cover only part of a chunk @@ -1051,33 +1085,20 @@ Operand Below(Operand high, Operand low) context.Copy(selected, index); context.MarkLabel(lblFound); - context.Store(StorageKind.LocalMemory, lastBinding, selected); + + // The chosen entry (its start is at or below the address) replaces the cache; an address past its end is refused below, and a later access + // inside it hits the cache Operand found = context.IMultiply(selected, Const(GmemMapEntryWords)); - Operand lblSkip = Label(); - Operand inPool = Below(MapWord(found, 3), MapWord(found, 2)); - if (vectorCount > 1) + for (int word = 0; word < 6; word++) { - // The resolver chose the entry from word zero. Require the - // vector's last byte to remain below its end as well, rather - // than relying on host pool alignment. - Operand lastLow = context.IAdd(addrLow, Const(vectorCount * 4 - 1)); - Operand endCarry = context.ConditionalSelect( - context.ICompareLessUnsigned(lastLow, addrLow), Const(1), Const(0)); - Operand lastHigh = context.IAdd(addrHigh, endCarry); - Operand endLow = MapWord(found, 2); - Operand endHigh = MapWord(found, 3); - inPool = context.BitwiseOr( - context.ICompareLessUnsigned(lastHigh, endHigh), - context.BitwiseAnd( - context.ICompareEqual(lastHigh, endHigh), - context.ICompareLessUnsigned(lastLow, endLow))); + context.Store(StorageKind.LocalMemory, cacheBinding, Const(word), MapWord(found, word)); } - context.BranchIfFalse(lblSkip, inPool); + Operand lblSkip = Label(); + context.BranchIfFalse(lblSkip, LastBelow(CacheWord(3), CacheWord(2))); context.MarkLabel(lblChosen); - found = context.IMultiply(selected, Const(GmemMapEntryWords)); if (writes) { @@ -1100,9 +1121,9 @@ Operand Below(Operand high, Operand low) // A small integer is read or written in its 32-bit word Operand wordLow = isSmall ? context.BitwiseAnd(addrLow, Const(~3)) : addrLow; - Operand hostLow = context.IAdd(wordLow, MapWord(found, 4)); + Operand hostLow = context.IAdd(wordLow, CacheWord(4)); Operand carry = context.ConditionalSelect(context.ICompareLessUnsigned(hostLow, wordLow), Const(1), Const(0)); - Operand hostHigh = context.IAdd(context.IAdd(addrHigh, MapWord(found, 5)), carry); + Operand hostHigh = context.IAdd(context.IAdd(addrHigh, CacheWord(5)), carry); Operand resultValue = null; @@ -1176,7 +1197,6 @@ Operand Below(Operand high, Operand low) } context.MarkLabel(lblSkip); - context.Store(StorageKind.LocalMemory, lastBinding, Const(0)); bool returnsValue = operation.Dest != null; diff --git a/dm/third_party/ryujinx/src/Ryujinx.Graphics.Shader/Translation/ResourceManager.cs b/dm/third_party/ryujinx/src/Ryujinx.Graphics.Shader/Translation/ResourceManager.cs index deb9ce19..e28d988e 100644 --- a/dm/third_party/ryujinx/src/Ryujinx.Graphics.Shader/Translation/ResourceManager.cs +++ b/dm/third_party/ryujinx/src/Ryujinx.Graphics.Shader/Translation/ResourceManager.cs @@ -160,6 +160,13 @@ public void SetVertexAsComputeLocalMemories(ShaderStage stage, InputTopology inp } // nxnative: the pool table entry a raw global memory helper chose last in this invocation (GlobalToStorage.TryGenerateRawStorageOp), one word for each helper, zero at the start + // The table entry a raw access helper resolved last in this invocation: its start, end and delta words (map words 0..5), so a hit compares + // against registers instead of loading the entry again. Zero-initialized: end 0 holds no address + public int AddRawEntryCacheMemory() + { + return Properties.AddLocalMemory(new MemoryDefinition($"gmem_entry_{Properties.LocalMemories.Count}", AggregateType.Array | AggregateType.U32, 6, zeroInit: true)); + } + public int AddRawLastEntryMemory() { return Properties.AddLocalMemory(new MemoryDefinition($"gmem_last_{Properties.LocalMemories.Count}", AggregateType.U32, 1, zeroInit: true)); From 26454578ed203c366315881a7706b0d9b0893bd1 Mon Sep 17 00:00:00 2001 From: seedds Date: Fri, 13 Nov 2026 18:13:07 +0100 Subject: [PATCH 2/3] shader: one raw entry cache per shader; drop the last-index cache Every raw helper resolves through the same immutable table, so one six-word cache per shader is enough. A store after a load to the same pool hits too, and only one zero-initialised array is live. The old one-word last-index memory (AddRawLastEntryMemory) and its bindings are no longer used, so they are removed. The 520 Ocarina task/mesh modules are byte-identical to the per-kind build measured in the PR. These shaders have one raw access kind, so the in-run A/B result applies unchanged. Co-Authored-By: Claude Opus 5.5 --- .../Optimizations/GlobalToStorage.cs | 31 +++++-------------- .../Translation/ResourceManager.cs | 9 ++---- 2 files changed, 10 insertions(+), 30 deletions(-) diff --git a/dm/third_party/ryujinx/src/Ryujinx.Graphics.Shader/Translation/Optimizations/GlobalToStorage.cs b/dm/third_party/ryujinx/src/Ryujinx.Graphics.Shader/Translation/Optimizations/GlobalToStorage.cs index 193fee01..be9e1a08 100644 --- a/dm/third_party/ryujinx/src/Ryujinx.Graphics.Shader/Translation/Optimizations/GlobalToStorage.cs +++ b/dm/third_party/ryujinx/src/Ryujinx.Graphics.Shader/Translation/Optimizations/GlobalToStorage.cs @@ -73,7 +73,6 @@ public bool Equals(LsKey other) private readonly List _entries; private readonly Dictionary> _sharedEntries; - private readonly Dictionary<(Instruction, StorageKind), int> _rawLastBindings; private readonly Dictionary _rawVectorFunctions; private readonly HelperFunctionManager _hfm; @@ -81,7 +80,6 @@ public GtsContext(HelperFunctionManager hfm) { _entries = []; _sharedEntries = new Dictionary>(); - _rawLastBindings = new Dictionary<(Instruction, StorageKind), int>(); _rawVectorFunctions = []; _hfm = hfm; } @@ -98,33 +96,20 @@ public int AddRawVectorFunction(int count, Function function) return functionId; } - private readonly Dictionary<(Instruction, StorageKind), int> _rawEntryCacheBindings = new(); + private int _rawEntryCacheBinding = -1; - public int GetOrAddRawEntryCacheBinding(ResourceManager resourceManager, Operation operation) + // One cache for the shader: every raw helper resolves through the same immutable table, so a store after a load to the same pool hits + // too, and only one zero-initialised array stays live + public int GetOrAddRawEntryCacheBinding(ResourceManager resourceManager) { - var key = (operation.Inst, operation.StorageKind); - - if (!_rawEntryCacheBindings.TryGetValue(key, out int binding)) + if (_rawEntryCacheBinding < 0) { - binding = resourceManager.AddRawEntryCacheMemory(); - _rawEntryCacheBindings.Add(key, binding); + _rawEntryCacheBinding = resourceManager.AddRawEntryCacheMemory(); } - return binding; + return _rawEntryCacheBinding; } - public int GetOrAddRawLastBinding(ResourceManager resourceManager, Operation operation) - { - var key = (operation.Inst, operation.StorageKind); - - if (!_rawLastBindings.TryGetValue(key, out int binding)) - { - binding = resourceManager.AddRawLastEntryMemory(); - _rawLastBindings.Add(key, binding); - } - - return binding; - } public int AddFunction(Operation baseOp, bool isMultiTarget, IReadOnlyList targetCbs, Function function) { @@ -1024,7 +1009,7 @@ Operand Below(Operand high, Operand low) // The entry this helper resolved last in this invocation, kept as its start, end and delta words (ResourceManager.AddRawEntryCacheMemory): // an address inside it, and for a vector its last byte too, takes it without loading the table at all. The cache starts zero (end 0), so the // first lookup searches. The host binds one disjoint table for the draw. - int cacheBinding = gtsContext.GetOrAddRawEntryCacheBinding(resourceManager, operation); + int cacheBinding = gtsContext.GetOrAddRawEntryCacheBinding(resourceManager); Operand CacheWord(int word) { diff --git a/dm/third_party/ryujinx/src/Ryujinx.Graphics.Shader/Translation/ResourceManager.cs b/dm/third_party/ryujinx/src/Ryujinx.Graphics.Shader/Translation/ResourceManager.cs index e28d988e..87167f9a 100644 --- a/dm/third_party/ryujinx/src/Ryujinx.Graphics.Shader/Translation/ResourceManager.cs +++ b/dm/third_party/ryujinx/src/Ryujinx.Graphics.Shader/Translation/ResourceManager.cs @@ -160,18 +160,13 @@ public void SetVertexAsComputeLocalMemories(ShaderStage stage, InputTopology inp } // nxnative: the pool table entry a raw global memory helper chose last in this invocation (GlobalToStorage.TryGenerateRawStorageOp), one word for each helper, zero at the start - // The table entry a raw access helper resolved last in this invocation: its start, end and delta words (map words 0..5), so a hit compares - // against registers instead of loading the entry again. Zero-initialized: end 0 holds no address + // The table entry the shader's raw access helpers resolved last in this invocation: its start, end and delta words (map words 0..5), so a + // hit compares against registers instead of loading the entry again. Zero-initialized: end 0 holds no address public int AddRawEntryCacheMemory() { return Properties.AddLocalMemory(new MemoryDefinition($"gmem_entry_{Properties.LocalMemories.Count}", AggregateType.Array | AggregateType.U32, 6, zeroInit: true)); } - public int AddRawLastEntryMemory() - { - return Properties.AddLocalMemory(new MemoryDefinition($"gmem_last_{Properties.LocalMemories.Count}", AggregateType.U32, 1, zeroInit: true)); - } - // Words 1..3 returned by one raw vector load. Followers consume them // immediately, before another memory access can overwrite the array. private int _rawVectorMemoryId = -1; From 9b1e210e08bef46483d5150212292e0836a5bdab Mon Sep 17 00:00:00 2001 From: seedds Date: Fri, 13 Nov 2026 18:14:21 +0100 Subject: [PATCH 3/3] shader: describe the per-shader raw entry cache; fields together Comment and field placement only: the six-word cache comment replaces the stale one-word last-index description and documents the vector last-byte bound. Translator output is unchanged. Co-Authored-By: Claude Opus 5.5 --- .../Translation/Optimizations/GlobalToStorage.cs | 3 +-- .../Ryujinx.Graphics.Shader/Translation/ResourceManager.cs | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/dm/third_party/ryujinx/src/Ryujinx.Graphics.Shader/Translation/Optimizations/GlobalToStorage.cs b/dm/third_party/ryujinx/src/Ryujinx.Graphics.Shader/Translation/Optimizations/GlobalToStorage.cs index be9e1a08..0f0c4155 100644 --- a/dm/third_party/ryujinx/src/Ryujinx.Graphics.Shader/Translation/Optimizations/GlobalToStorage.cs +++ b/dm/third_party/ryujinx/src/Ryujinx.Graphics.Shader/Translation/Optimizations/GlobalToStorage.cs @@ -75,6 +75,7 @@ public bool Equals(LsKey other) private readonly Dictionary> _sharedEntries; private readonly Dictionary _rawVectorFunctions; private readonly HelperFunctionManager _hfm; + private int _rawEntryCacheBinding = -1; public GtsContext(HelperFunctionManager hfm) { @@ -96,8 +97,6 @@ public int AddRawVectorFunction(int count, Function function) return functionId; } - private int _rawEntryCacheBinding = -1; - // One cache for the shader: every raw helper resolves through the same immutable table, so a store after a load to the same pool hits // too, and only one zero-initialised array stays live public int GetOrAddRawEntryCacheBinding(ResourceManager resourceManager) diff --git a/dm/third_party/ryujinx/src/Ryujinx.Graphics.Shader/Translation/ResourceManager.cs b/dm/third_party/ryujinx/src/Ryujinx.Graphics.Shader/Translation/ResourceManager.cs index 87167f9a..77533a41 100644 --- a/dm/third_party/ryujinx/src/Ryujinx.Graphics.Shader/Translation/ResourceManager.cs +++ b/dm/third_party/ryujinx/src/Ryujinx.Graphics.Shader/Translation/ResourceManager.cs @@ -159,9 +159,9 @@ public void SetVertexAsComputeLocalMemories(ShaderStage stage, InputTopology inp } } - // nxnative: the pool table entry a raw global memory helper chose last in this invocation (GlobalToStorage.TryGenerateRawStorageOp), one word for each helper, zero at the start - // The table entry the shader's raw access helpers resolved last in this invocation: its start, end and delta words (map words 0..5), so a - // hit compares against registers instead of loading the entry again. Zero-initialized: end 0 holds no address + // nxnative: the pool table entry the shader's raw global memory helpers (GlobalToStorage.TryGenerateRawStorageOp) resolved last in this + // invocation: its start, end and delta words (map words 0..5), so a hit compares against registers instead of loading the entry again. One + // array per shader, zero-initialized: end 0 holds no address. A vector hit also requires its last byte below the cached end public int AddRawEntryCacheMemory() { return Properties.AddLocalMemory(new MemoryDefinition($"gmem_entry_{Properties.LocalMemories.Count}", AggregateType.Array | AggregateType.U32, 6, zeroInit: true));