From 1c7a90ef359d9974e5bd257c4d8e9bf526a6966c Mon Sep 17 00:00:00 2001
From: gdkchan <gab.dark.100@gmail.com>
Date: Mon, 3 Jul 2023 14:29:27 -0300
Subject: [PATCH] Stop identifying shader textures with handle and cbuf, use
 binding instead (#5266)

* Stop identifying shader textures with handle and cbuf, use binding instead

* Remove now unused code

* Consider image operations as having accurate type information too

I don't know why that was not the case before

* Fix missing unscale on InsertCoordNormalization, stop calling SetUsageFlagsForTextureQuery when not needed

* Shader cache version bump

* Change get texture methods to return descriptors created from ResourceManager state

 This is required to ensure that reserved textures and images will not be bound as a guest texture/image

* Fix BindlessElimination.SetHandle inserting coords at the wrong place
---
 .../Shader/DiskCache/DiskCacheHostStorage.cs  |   2 +-
 .../BufferDescriptor.cs                       |  15 +-
 .../CodeGen/Glsl/Declarations.cs              |  79 ++---
 .../CodeGen/Glsl/DefaultNames.cs              |   3 -
 .../Glsl/Instructions/InstGenMemory.cs        |  36 ++-
 .../CodeGen/Glsl/OperandManager.cs            |  58 +---
 .../CodeGen/Spirv/CodeGenContext.cs           |   6 +-
 .../CodeGen/Spirv/Declarations.cs             |  77 ++---
 .../CodeGen/Spirv/Instructions.cs             |  20 +-
 .../CodeGen/Spirv/TextureMeta.cs              |   4 -
 .../Instructions/InstEmitSurface.cs           |  66 ++--
 .../Instructions/InstEmitTexture.cs           | 132 ++++----
 .../TextureOperation.cs                       |  30 +-
 .../StructuredIr/AstTextureOperation.cs       |   9 +-
 .../StructuredIr/ShaderProperties.cs          |  20 +-
 .../StructuredIr/StructuredProgram.cs         |  10 +-
 .../StructuredIr/TextureDefinition.cs         |  27 ++
 .../TextureDescriptor.cs                      |  13 +-
 .../Translation/EmitterContext.cs             |  30 --
 .../Translation/EmitterContextInsts.cs        |  81 +++++
 .../Optimizations/BindlessElimination.cs      |  14 +-
 .../Optimizations/BindlessToIndexed.cs        |  15 +-
 .../Translation/ResourceManager.cs            | 286 +++++++++++++++++-
 .../Translation/Rewriter.cs                   |  54 ++--
 .../Translation/ShaderConfig.cs               | 228 +-------------
 25 files changed, 656 insertions(+), 659 deletions(-)
 delete mode 100644 src/Ryujinx.Graphics.Shader/CodeGen/Spirv/TextureMeta.cs
 create mode 100644 src/Ryujinx.Graphics.Shader/StructuredIr/TextureDefinition.cs

diff --git a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs
index 95a0a6bdd..672b3b8d1 100644
--- a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs
+++ b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs
@@ -22,7 +22,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
         private const ushort FileFormatVersionMajor = 1;
         private const ushort FileFormatVersionMinor = 2;
         private const uint FileFormatVersionPacked = ((uint)FileFormatVersionMajor << 16) | FileFormatVersionMinor;
-        private const uint CodeGenVersion = 5311;
+        private const uint CodeGenVersion = 5266;
 
         private const string SharedTocFileName = "shared.toc";
         private const string SharedDataFileName = "shared.data";
diff --git a/src/Ryujinx.Graphics.Shader/BufferDescriptor.cs b/src/Ryujinx.Graphics.Shader/BufferDescriptor.cs
index d1da95393..d8cad1df8 100644
--- a/src/Ryujinx.Graphics.Shader/BufferDescriptor.cs
+++ b/src/Ryujinx.Graphics.Shader/BufferDescriptor.cs
@@ -8,7 +8,7 @@ namespace Ryujinx.Graphics.Shader
         public readonly byte Slot;
         public readonly byte SbCbSlot;
         public readonly ushort SbCbOffset;
-        public BufferUsageFlags Flags;
+        public readonly BufferUsageFlags Flags;
 
         public BufferDescriptor(int binding, int slot)
         {
@@ -16,25 +16,16 @@ namespace Ryujinx.Graphics.Shader
             Slot = (byte)slot;
             SbCbSlot = 0;
             SbCbOffset = 0;
-
             Flags = BufferUsageFlags.None;
         }
 
-        public BufferDescriptor(int binding, int slot, int sbCbSlot, int sbCbOffset)
+        public BufferDescriptor(int binding, int slot, int sbCbSlot, int sbCbOffset, BufferUsageFlags flags)
         {
             Binding = binding;
             Slot = (byte)slot;
             SbCbSlot = (byte)sbCbSlot;
             SbCbOffset = (ushort)sbCbOffset;
-
-            Flags = BufferUsageFlags.None;
-        }
-
-        public BufferDescriptor SetFlag(BufferUsageFlags flag)
-        {
-            Flags |= flag;
-
-            return this;
+            Flags = flags;
         }
     }
 }
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Declarations.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Declarations.cs
index 94b850e7b..2370b49f0 100644
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Declarations.cs
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Declarations.cs
@@ -75,22 +75,8 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
             DeclareStorageBuffers(context, context.Config.Properties.StorageBuffers.Values);
             DeclareMemories(context, context.Config.Properties.LocalMemories.Values, isShared: false);
             DeclareMemories(context, context.Config.Properties.SharedMemories.Values, isShared: true);
-
-            var textureDescriptors = context.Config.GetTextureDescriptors();
-            if (textureDescriptors.Length != 0)
-            {
-                DeclareSamplers(context, textureDescriptors);
-
-                context.AppendLine();
-            }
-
-            var imageDescriptors = context.Config.GetImageDescriptors();
-            if (imageDescriptors.Length != 0)
-            {
-                DeclareImages(context, imageDescriptors);
-
-                context.AppendLine();
-            }
+            DeclareSamplers(context, context.Config.Properties.Textures.Values);
+            DeclareImages(context, context.Config.Properties.Images.Values);
 
             if (context.Config.Stage != ShaderStage.Compute)
             {
@@ -369,80 +355,71 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
             }
         }
 
-        private static void DeclareSamplers(CodeGenContext context, TextureDescriptor[] descriptors)
+        private static void DeclareSamplers(CodeGenContext context, IEnumerable<TextureDefinition> definitions)
         {
             int arraySize = 0;
-            foreach (var descriptor in descriptors)
+
+            foreach (var definition in definitions)
             {
-                if (descriptor.Type.HasFlag(SamplerType.Indexed))
+                string indexExpr = string.Empty;
+
+                if (definition.Type.HasFlag(SamplerType.Indexed))
                 {
                     if (arraySize == 0)
                     {
-                        arraySize = ShaderConfig.SamplerArraySize;
+                        arraySize = ResourceManager.SamplerArraySize;
                     }
                     else if (--arraySize != 0)
                     {
                         continue;
                     }
+
+                    indexExpr = $"[{NumberFormatter.FormatInt(arraySize)}]";
                 }
 
-                string indexExpr = NumberFormatter.FormatInt(arraySize);
-
-                string samplerName = OperandManager.GetSamplerName(
-                    context.Config.Stage,
-                    descriptor.CbufSlot,
-                    descriptor.HandleIndex,
-                    descriptor.Type.HasFlag(SamplerType.Indexed),
-                    indexExpr);
-
-                string samplerTypeName = descriptor.Type.ToGlslSamplerType();
+                string samplerTypeName = definition.Type.ToGlslSamplerType();
 
                 string layout = string.Empty;
 
                 if (context.Config.Options.TargetApi == TargetApi.Vulkan)
                 {
-                    layout = ", set = 2";
+                    layout = $", set = {definition.Set}";
                 }
 
-                context.AppendLine($"layout (binding = {descriptor.Binding}{layout}) uniform {samplerTypeName} {samplerName};");
+                context.AppendLine($"layout (binding = {definition.Binding}{layout}) uniform {samplerTypeName} {definition.Name}{indexExpr};");
             }
         }
 
-        private static void DeclareImages(CodeGenContext context, TextureDescriptor[] descriptors)
+        private static void DeclareImages(CodeGenContext context, IEnumerable<TextureDefinition> definitions)
         {
             int arraySize = 0;
-            foreach (var descriptor in descriptors)
+
+            foreach (var definition in definitions)
             {
-                if (descriptor.Type.HasFlag(SamplerType.Indexed))
+                string indexExpr = string.Empty;
+
+                if (definition.Type.HasFlag(SamplerType.Indexed))
                 {
                     if (arraySize == 0)
                     {
-                        arraySize = ShaderConfig.SamplerArraySize;
+                        arraySize = ResourceManager.SamplerArraySize;
                     }
                     else if (--arraySize != 0)
                     {
                         continue;
                     }
+
+                    indexExpr = $"[{NumberFormatter.FormatInt(arraySize)}]";
                 }
 
-                string indexExpr = NumberFormatter.FormatInt(arraySize);
+                string imageTypeName = definition.Type.ToGlslImageType(definition.Format.GetComponentType());
 
-                string imageName = OperandManager.GetImageName(
-                    context.Config.Stage,
-                    descriptor.CbufSlot,
-                    descriptor.HandleIndex,
-                    descriptor.Format,
-                    descriptor.Type.HasFlag(SamplerType.Indexed),
-                    indexExpr);
-
-                string imageTypeName = descriptor.Type.ToGlslImageType(descriptor.Format.GetComponentType());
-
-                if (descriptor.Flags.HasFlag(TextureUsageFlags.ImageCoherent))
+                if (definition.Flags.HasFlag(TextureUsageFlags.ImageCoherent))
                 {
                     imageTypeName = "coherent " + imageTypeName;
                 }
 
-                string layout = descriptor.Format.ToGlslFormat();
+                string layout = definition.Format.ToGlslFormat();
 
                 if (!string.IsNullOrEmpty(layout))
                 {
@@ -451,10 +428,10 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
 
                 if (context.Config.Options.TargetApi == TargetApi.Vulkan)
                 {
-                    layout = $", set = 3{layout}";
+                    layout = $", set = {definition.Set}{layout}";
                 }
 
-                context.AppendLine($"layout (binding = {descriptor.Binding}{layout}) uniform {imageTypeName} {imageName};");
+                context.AppendLine($"layout (binding = {definition.Binding}{layout}) uniform {imageTypeName} {definition.Name}{indexExpr};");
             }
         }
 
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/DefaultNames.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/DefaultNames.cs
index 842228edf..54bf9aeb2 100644
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/DefaultNames.cs
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/DefaultNames.cs
@@ -4,9 +4,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
     {
         public const string LocalNamePrefix = "temp";
 
-        public const string SamplerNamePrefix = "tex";
-        public const string ImageNamePrefix = "img";
-
         public const string PerPatchAttributePrefix = "patch_attr_";
         public const string IAttributePrefix = "in_attr";
         public const string OAttributePrefix = "out_attr";
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenMemory.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenMemory.cs
index e0faed298..7e6d8bb5c 100644
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenMemory.cs
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenMemory.cs
@@ -84,7 +84,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
                 indexExpr = Src(AggregateType.S32);
             }
 
-            string imageName = OperandManager.GetImageName(context.Config.Stage, texOp, indexExpr);
+            string imageName = GetImageName(context.Config, texOp, indexExpr);
 
             texCallBuilder.Append('(');
             texCallBuilder.Append(imageName);
@@ -216,7 +216,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
                 indexExpr = GetSoureExpr(context, texOp.GetSource(0), AggregateType.S32);
             }
 
-            string samplerName = OperandManager.GetSamplerName(context.Config.Stage, texOp, indexExpr);
+            string samplerName = GetSamplerName(context.Config, texOp, indexExpr);
 
             int coordsIndex = isBindless || isIndexed ? 1 : 0;
 
@@ -342,7 +342,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
                 indexExpr = Src(AggregateType.S32);
             }
 
-            string samplerName = OperandManager.GetSamplerName(context.Config.Stage, texOp, indexExpr);
+            string samplerName = GetSamplerName(context.Config, texOp, indexExpr);
 
             texCall += "(" + samplerName;
 
@@ -538,7 +538,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
                 indexExpr = GetSoureExpr(context, texOp.GetSource(0), AggregateType.S32);
             }
 
-            string samplerName = OperandManager.GetSamplerName(context.Config.Stage, texOp, indexExpr);
+            string samplerName = GetSamplerName(context.Config, texOp, indexExpr);
 
             if (texOp.Index == 3)
             {
@@ -546,8 +546,8 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
             }
             else
             {
-                TextureDescriptor descriptor = context.Config.FindTextureDescriptor(texOp);
-                bool hasLod = !descriptor.Type.HasFlag(SamplerType.Multisample) && descriptor.Type != SamplerType.TextureBuffer;
+                context.Config.Properties.Textures.TryGetValue(texOp.Binding, out TextureDefinition definition);
+                bool hasLod = !definition.Type.HasFlag(SamplerType.Multisample) && (definition.Type & SamplerType.Mask) != SamplerType.TextureBuffer;
                 string texCall;
 
                 if (hasLod)
@@ -715,6 +715,30 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
             return varName;
         }
 
+        private static string GetSamplerName(ShaderConfig config, AstTextureOperation texOp, string indexExpr)
+        {
+            string name = config.Properties.Textures[texOp.Binding].Name;
+
+            if (texOp.Type.HasFlag(SamplerType.Indexed))
+            {
+                name = $"{name}[{indexExpr}]";
+            }
+
+            return name;
+        }
+
+        private static string GetImageName(ShaderConfig config, AstTextureOperation texOp, string indexExpr)
+        {
+            string name = config.Properties.Images[texOp.Binding].Name;
+
+            if (texOp.Type.HasFlag(SamplerType.Indexed))
+            {
+                name = $"{name}[{indexExpr}]";
+            }
+
+            return name;
+        }
+
         private static string GetMask(int index)
         {
             return $".{"rgba".AsSpan(index, 1)}";
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/OperandManager.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/OperandManager.cs
index 0ca3b55fc..17ffad9a5 100644
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/OperandManager.cs
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/OperandManager.cs
@@ -11,9 +11,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
 {
     class OperandManager
     {
-        private static readonly string[] _stagePrefixes = new string[] { "cp", "vp", "tcp", "tep", "gp", "fp" };
-
-        private readonly Dictionary<AstOperand, string> _locals;
+        private Dictionary<AstOperand, string> _locals;
 
         public OperandManager()
         {
@@ -41,60 +39,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
             };
         }
 
-        public static string GetSamplerName(ShaderStage stage, AstTextureOperation texOp, string indexExpr)
-        {
-            return GetSamplerName(stage, texOp.CbufSlot, texOp.Handle, texOp.Type.HasFlag(SamplerType.Indexed), indexExpr);
-        }
-
-        public static string GetSamplerName(ShaderStage stage, int cbufSlot, int handle, bool indexed, string indexExpr)
-        {
-            string suffix = cbufSlot < 0 ? $"_tcb_{handle:X}" : $"_cb{cbufSlot}_{handle:X}";
-
-            if (indexed)
-            {
-                suffix += $"a[{indexExpr}]";
-            }
-
-            return GetShaderStagePrefix(stage) + "_" + DefaultNames.SamplerNamePrefix + suffix;
-        }
-
-        public static string GetImageName(ShaderStage stage, AstTextureOperation texOp, string indexExpr)
-        {
-            return GetImageName(stage, texOp.CbufSlot, texOp.Handle, texOp.Format, texOp.Type.HasFlag(SamplerType.Indexed), indexExpr);
-        }
-
-        public static string GetImageName(
-            ShaderStage stage,
-            int cbufSlot,
-            int handle,
-            TextureFormat format,
-            bool indexed,
-            string indexExpr)
-        {
-            string suffix = cbufSlot < 0
-                ? $"_tcb_{handle:X}_{format.ToGlslFormat()}"
-                : $"_cb{cbufSlot}_{handle:X}_{format.ToGlslFormat()}";
-
-            if (indexed)
-            {
-                suffix += $"a[{indexExpr}]";
-            }
-
-            return GetShaderStagePrefix(stage) + "_" + DefaultNames.ImageNamePrefix + suffix;
-        }
-
-        public static string GetShaderStagePrefix(ShaderStage stage)
-        {
-            int index = (int)stage;
-
-            if ((uint)index >= _stagePrefixes.Length)
-            {
-                return "invalid";
-            }
-
-            return _stagePrefixes[index];
-        }
-
         public static string GetArgumentName(int argIndex)
         {
             return $"{DefaultNames.ArgumentNamePrefix}{argIndex}";
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/CodeGenContext.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/CodeGenContext.cs
index 9956e90a3..d4f490458 100644
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/CodeGenContext.cs
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/CodeGenContext.cs
@@ -28,9 +28,9 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
         public Dictionary<int, Instruction> StorageBuffers { get; } = new Dictionary<int, Instruction>();
         public Dictionary<int, Instruction> LocalMemories { get; } = new Dictionary<int, Instruction>();
         public Dictionary<int, Instruction> SharedMemories { get; } = new Dictionary<int, Instruction>();
-        public Dictionary<TextureMeta, SamplerType> SamplersTypes { get; } = new Dictionary<TextureMeta, SamplerType>();
-        public Dictionary<TextureMeta, (Instruction, Instruction, Instruction)> Samplers { get; } = new Dictionary<TextureMeta, (Instruction, Instruction, Instruction)>();
-        public Dictionary<TextureMeta, (Instruction, Instruction)> Images { get; } = new Dictionary<TextureMeta, (Instruction, Instruction)>();
+        public Dictionary<int, SamplerType> SamplersTypes { get; } = new Dictionary<int, SamplerType>();
+        public Dictionary<int, (Instruction, Instruction, Instruction)> Samplers { get; } = new Dictionary<int, (Instruction, Instruction, Instruction)>();
+        public Dictionary<int, (Instruction, Instruction)> Images { get; } = new Dictionary<int, (Instruction, Instruction)>();
         public Dictionary<IoDefinition, Instruction> Inputs { get; } = new Dictionary<IoDefinition, Instruction>();
         public Dictionary<IoDefinition, Instruction> Outputs { get; } = new Dictionary<IoDefinition, Instruction>();
         public Dictionary<IoDefinition, Instruction> InputsPerPatch { get; } = new Dictionary<IoDefinition, Instruction>();
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Declarations.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Declarations.cs
index da1e385a7..2c849cd49 100644
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Declarations.cs
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Declarations.cs
@@ -72,8 +72,8 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
             DeclareStorageBuffers(context, context.Config.Properties.StorageBuffers.Values);
             DeclareMemories(context, context.Config.Properties.LocalMemories, context.LocalMemories, StorageClass.Private);
             DeclareMemories(context, context.Config.Properties.SharedMemories, context.SharedMemories, StorageClass.Workgroup);
-            DeclareSamplers(context, context.Config.GetTextureDescriptors());
-            DeclareImages(context, context.Config.GetImageDescriptors());
+            DeclareSamplers(context, context.Config.Properties.Textures.Values);
+            DeclareImages(context, context.Config.Properties.Images.Values);
             DeclareInputsAndOutputs(context, info);
         }
 
@@ -110,6 +110,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
 
             foreach (BufferDefinition buffer in buffers)
             {
+                int setIndex = context.Config.Options.TargetApi == TargetApi.Vulkan ? buffer.Set : 0;
                 int alignment = buffer.Layout == BufferLayout.Std140 ? 16 : 4;
                 int alignmentMask = alignment - 1;
                 int offset = 0;
@@ -163,7 +164,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
                 var variable = context.Variable(pointerType, StorageClass.Uniform);
 
                 context.Name(variable, buffer.Name);
-                context.Decorate(variable, Decoration.DescriptorSet, (LiteralInteger)buffer.Set);
+                context.Decorate(variable, Decoration.DescriptorSet, (LiteralInteger)setIndex);
                 context.Decorate(variable, Decoration.Binding, (LiteralInteger)buffer.Binding);
                 context.AddGlobalVariable(variable);
 
@@ -178,92 +179,72 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
             }
         }
 
-        private static void DeclareSamplers(CodeGenContext context, TextureDescriptor[] descriptors)
+        private static void DeclareSamplers(CodeGenContext context, IEnumerable<TextureDefinition> samplers)
         {
-            foreach (var descriptor in descriptors)
+            foreach (var sampler in samplers)
             {
-                var meta = new TextureMeta(descriptor.CbufSlot, descriptor.HandleIndex, descriptor.Format);
+                int setIndex = context.Config.Options.TargetApi == TargetApi.Vulkan ? sampler.Set : 0;
 
-                if (context.Samplers.ContainsKey(meta))
-                {
-                    continue;
-                }
-
-                int setIndex = context.Config.Options.TargetApi == TargetApi.Vulkan ? 2 : 0;
-
-                var dim = (descriptor.Type & SamplerType.Mask) switch
+                var dim = (sampler.Type & SamplerType.Mask) switch
                 {
                     SamplerType.Texture1D => Dim.Dim1D,
                     SamplerType.Texture2D => Dim.Dim2D,
                     SamplerType.Texture3D => Dim.Dim3D,
                     SamplerType.TextureCube => Dim.Cube,
                     SamplerType.TextureBuffer => Dim.Buffer,
-                    _ => throw new InvalidOperationException($"Invalid sampler type \"{descriptor.Type & SamplerType.Mask}\"."),
+                    _ => throw new InvalidOperationException($"Invalid sampler type \"{sampler.Type & SamplerType.Mask}\".")
                 };
 
                 var imageType = context.TypeImage(
                     context.TypeFP32(),
                     dim,
-                    descriptor.Type.HasFlag(SamplerType.Shadow),
-                    descriptor.Type.HasFlag(SamplerType.Array),
-                    descriptor.Type.HasFlag(SamplerType.Multisample),
+                    sampler.Type.HasFlag(SamplerType.Shadow),
+                    sampler.Type.HasFlag(SamplerType.Array),
+                    sampler.Type.HasFlag(SamplerType.Multisample),
                     1,
                     ImageFormat.Unknown);
 
-                var nameSuffix = meta.CbufSlot < 0 ? $"_tcb_{meta.Handle:X}" : $"_cb{meta.CbufSlot}_{meta.Handle:X}";
-
                 var sampledImageType = context.TypeSampledImage(imageType);
                 var sampledImagePointerType = context.TypePointer(StorageClass.UniformConstant, sampledImageType);
                 var sampledImageVariable = context.Variable(sampledImagePointerType, StorageClass.UniformConstant);
 
-                context.Samplers.Add(meta, (imageType, sampledImageType, sampledImageVariable));
-                context.SamplersTypes.Add(meta, descriptor.Type);
+                context.Samplers.Add(sampler.Binding, (imageType, sampledImageType, sampledImageVariable));
+                context.SamplersTypes.Add(sampler.Binding, sampler.Type);
 
-                context.Name(sampledImageVariable, $"{GetStagePrefix(context.Config.Stage)}_tex{nameSuffix}");
+                context.Name(sampledImageVariable, sampler.Name);
                 context.Decorate(sampledImageVariable, Decoration.DescriptorSet, (LiteralInteger)setIndex);
-                context.Decorate(sampledImageVariable, Decoration.Binding, (LiteralInteger)descriptor.Binding);
+                context.Decorate(sampledImageVariable, Decoration.Binding, (LiteralInteger)sampler.Binding);
                 context.AddGlobalVariable(sampledImageVariable);
             }
         }
 
-        private static void DeclareImages(CodeGenContext context, TextureDescriptor[] descriptors)
+        private static void DeclareImages(CodeGenContext context, IEnumerable<TextureDefinition> images)
         {
-            foreach (var descriptor in descriptors)
+            foreach (var image in images)
             {
-                var meta = new TextureMeta(descriptor.CbufSlot, descriptor.HandleIndex, descriptor.Format);
+                int setIndex = context.Config.Options.TargetApi == TargetApi.Vulkan ? image.Set : 0;
 
-                if (context.Images.ContainsKey(meta))
-                {
-                    continue;
-                }
-
-                int setIndex = context.Config.Options.TargetApi == TargetApi.Vulkan ? 3 : 0;
-
-                var dim = GetDim(descriptor.Type);
+                var dim = GetDim(image.Type);
 
                 var imageType = context.TypeImage(
-                    context.GetType(meta.Format.GetComponentType()),
+                    context.GetType(image.Format.GetComponentType()),
                     dim,
-                    descriptor.Type.HasFlag(SamplerType.Shadow),
-                    descriptor.Type.HasFlag(SamplerType.Array),
-                    descriptor.Type.HasFlag(SamplerType.Multisample),
+                    image.Type.HasFlag(SamplerType.Shadow),
+                    image.Type.HasFlag(SamplerType.Array),
+                    image.Type.HasFlag(SamplerType.Multisample),
                     AccessQualifier.ReadWrite,
-                    GetImageFormat(meta.Format));
-
-                var nameSuffix = meta.CbufSlot < 0 ?
-                    $"_tcb_{meta.Handle:X}_{meta.Format.ToGlslFormat()}" :
-                    $"_cb{meta.CbufSlot}_{meta.Handle:X}_{meta.Format.ToGlslFormat()}";
+                    GetImageFormat(image.Format));
 
                 var imagePointerType = context.TypePointer(StorageClass.UniformConstant, imageType);
                 var imageVariable = context.Variable(imagePointerType, StorageClass.UniformConstant);
 
-                context.Images.Add(meta, (imageType, imageVariable));
+                context.Images.Add(image.Binding, (imageType, imageVariable));
 
-                context.Name(imageVariable, $"{GetStagePrefix(context.Config.Stage)}_img{nameSuffix}");
+                context.Name(imageVariable, image.Name);
                 context.Decorate(imageVariable, Decoration.DescriptorSet, (LiteralInteger)setIndex);
-                context.Decorate(imageVariable, Decoration.Binding, (LiteralInteger)descriptor.Binding);
+                context.Decorate(imageVariable, Decoration.Binding, (LiteralInteger)image.Binding);
 
-                if (descriptor.Flags.HasFlag(TextureUsageFlags.ImageCoherent))
+                if (image.Flags.HasFlag(TextureUsageFlags.ImageCoherent))
                 {
                     context.Decorate(imageVariable, Decoration.Coherent);
                 }
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs
index a53b40b24..9489397bc 100644
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs
@@ -657,7 +657,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
 
             SpvInstruction value = Src(componentType);
 
-            (SpvInstruction imageType, SpvInstruction imageVariable) = context.Images[new TextureMeta(texOp.CbufSlot, texOp.Handle, texOp.Format)];
+            (var imageType, var imageVariable) = context.Images[texOp.Binding];
 
             context.Load(imageType, imageVariable);
 
@@ -742,7 +742,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
                 pCoords = Src(AggregateType.S32);
             }
 
-            var (imageType, imageVariable) = context.Images[new TextureMeta(texOp.CbufSlot, texOp.Handle, texOp.Format)];
+            (var imageType, var imageVariable) = context.Images[texOp.Binding];
 
             var image = context.Load(imageType, imageVariable);
             var imageComponentType = context.GetType(componentType);
@@ -829,7 +829,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
 
             var texel = context.CompositeConstruct(context.TypeVector(context.GetType(componentType), ComponentsCount), cElems);
 
-            var (imageType, imageVariable) = context.Images[new TextureMeta(texOp.CbufSlot, texOp.Handle, texOp.Format)];
+            (var imageType, var imageVariable) = context.Images[texOp.Binding];
 
             var image = context.Load(imageType, imageVariable);
 
@@ -908,9 +908,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
                 pCoords = Src(AggregateType.FP32);
             }
 
-            var meta = new TextureMeta(texOp.CbufSlot, texOp.Handle, texOp.Format);
-
-            var (_, sampledImageType, sampledImageVariable) = context.Samplers[meta];
+            (_, var sampledImageType, var sampledImageVariable) = context.Samplers[texOp.Binding];
 
             var image = context.Load(sampledImageType, sampledImageVariable);
 
@@ -1511,9 +1509,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
 
             var resultType = colorIsVector ? context.TypeVector(context.TypeFP32(), 4) : context.TypeFP32();
 
-            var meta = new TextureMeta(texOp.CbufSlot, texOp.Handle, texOp.Format);
-
-            var (imageType, sampledImageType, sampledImageVariable) = context.Samplers[meta];
+            (var imageType, var sampledImageType, var sampledImageVariable) = context.Samplers[texOp.Binding];
 
             var image = context.Load(sampledImageType, sampledImageVariable);
 
@@ -1592,9 +1588,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
                 context.GetS32(texOp.GetSource(0));
             }
 
-            var meta = new TextureMeta(texOp.CbufSlot, texOp.Handle, texOp.Format);
-
-            (SpvInstruction imageType, SpvInstruction sampledImageType, SpvInstruction sampledImageVariable) = context.Samplers[meta];
+            (var imageType, var sampledImageType, var sampledImageVariable) = context.Samplers[texOp.Binding];
 
             var image = context.Load(sampledImageType, sampledImageVariable);
             image = context.Image(imageType, image);
@@ -1605,7 +1599,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
             }
             else
             {
-                var type = context.SamplersTypes[meta];
+                var type = context.SamplersTypes[texOp.Binding];
                 bool hasLod = !type.HasFlag(SamplerType.Multisample) && type != SamplerType.TextureBuffer;
 
                 int dimensions = (type & SamplerType.Mask) == SamplerType.TextureCube ? 2 : type.GetDimensions();
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/TextureMeta.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/TextureMeta.cs
deleted file mode 100644
index 56ea9a2a6..000000000
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/TextureMeta.cs
+++ /dev/null
@@ -1,4 +0,0 @@
-namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
-{
-    readonly record struct TextureMeta(int CbufSlot, int Handle, TextureFormat Format);
-}
diff --git a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitSurface.cs b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitSurface.cs
index 78fc313d8..0b929307e 100644
--- a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitSurface.cs
+++ b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitSurface.cs
@@ -218,7 +218,7 @@ namespace Ryujinx.Graphics.Shader.Instructions
                 return context.Copy(Register(srcB++, RegisterType.Gpr));
             }
 
-            Operand destOperand = dest != RegisterConsts.RegisterZeroIndex ? Register(dest, RegisterType.Gpr) : null;
+            Operand d = dest != RegisterConsts.RegisterZeroIndex ? Register(dest, RegisterType.Gpr) : null;
 
             List<Operand> sourcesList = new();
 
@@ -277,17 +277,17 @@ namespace Ryujinx.Graphics.Shader.Instructions
                 flags |= TextureFlags.Bindless;
             }
 
-            TextureOperation operation = context.CreateTextureOperation(
+            int binding = isBindless ? 0 : context.Config.ResourceManager.GetTextureOrImageBinding(
                 Instruction.ImageAtomic,
                 type,
                 format,
                 flags,
-                imm,
-                0,
-                new[] { destOperand },
-                sources);
+                TextureOperation.DefaultCbufSlot,
+                imm);
 
-            context.Add(operation);
+            Operand res = context.ImageAtomic(type, format, flags, binding, sources);
+
+            context.Copy(d, res);
         }
 
         private static void EmitSuld(
@@ -383,21 +383,17 @@ namespace Ryujinx.Graphics.Shader.Instructions
                     Array.Resize(ref dests, outputIndex);
                 }
 
-                TextureOperation operation = context.CreateTextureOperation(
+                TextureFormat format = isBindless ? TextureFormat.Unknown : context.Config.GetTextureFormat(handle);
+
+                int binding = isBindless ? 0 : context.Config.ResourceManager.GetTextureOrImageBinding(
                     Instruction.ImageLoad,
                     type,
+                    format,
                     flags,
-                    handle,
-                    (int)componentMask,
-                    dests,
-                    sources);
+                    TextureOperation.DefaultCbufSlot,
+                    handle);
 
-                if (!isBindless)
-                {
-                    operation.Format = context.Config.GetTextureFormat(handle);
-                }
-
-                context.Add(operation);
+                context.ImageLoad(type, format, flags, binding, (int)componentMask, dests, sources);
             }
             else
             {
@@ -430,17 +426,17 @@ namespace Ryujinx.Graphics.Shader.Instructions
                     Array.Resize(ref dests, outputIndex);
                 }
 
-                TextureOperation operation = context.CreateTextureOperation(
+                TextureFormat format = GetTextureFormat(size);
+
+                int binding = isBindless ? 0 : context.Config.ResourceManager.GetTextureOrImageBinding(
                     Instruction.ImageLoad,
                     type,
-                    GetTextureFormat(size),
+                    format,
                     flags,
-                    handle,
-                    compMask,
-                    dests,
-                    sources);
+                    TextureOperation.DefaultCbufSlot,
+                    handle);
 
-                context.Add(operation);
+                context.ImageLoad(type, format, flags, binding, compMask, dests, sources);
 
                 switch (size)
                 {
@@ -552,17 +548,15 @@ namespace Ryujinx.Graphics.Shader.Instructions
                 flags |= TextureFlags.Bindless;
             }
 
-            TextureOperation operation = context.CreateTextureOperation(
+            int binding = isBindless ? 0 : context.Config.ResourceManager.GetTextureOrImageBinding(
                 Instruction.ImageAtomic,
                 type,
                 format,
                 flags,
-                imm,
-                0,
-                null,
-                sources);
+                TextureOperation.DefaultCbufSlot,
+                imm);
 
-            context.Add(operation);
+            context.ImageAtomic(type, format, flags, binding, sources);
         }
 
         private static void EmitSust(
@@ -681,17 +675,15 @@ namespace Ryujinx.Graphics.Shader.Instructions
                 flags |= TextureFlags.Coherent;
             }
 
-            TextureOperation operation = context.CreateTextureOperation(
+            int binding = isBindless ? 0 : context.Config.ResourceManager.GetTextureOrImageBinding(
                 Instruction.ImageStore,
                 type,
                 format,
                 flags,
-                handle,
-                0,
-                null,
-                sources);
+                TextureOperation.DefaultCbufSlot,
+                handle);
 
-            context.Add(operation);
+            context.ImageStore(type, format, flags, binding, sources);
         }
 
         private static int GetComponentSizeInBytesLog2(SuatomSize size)
diff --git a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitTexture.cs b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitTexture.cs
index 3701325e2..7d3d22d8a 100644
--- a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitTexture.cs
+++ b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitTexture.cs
@@ -324,16 +324,7 @@ namespace Ryujinx.Graphics.Shader.Instructions
 
             int handle = !isBindless ? imm : 0;
 
-            TextureOperation operation = context.CreateTextureOperation(
-                Instruction.TextureSample,
-                type,
-                flags,
-                handle,
-                componentMask,
-                dests,
-                sources);
-
-            context.Add(operation);
+            EmitTextureSample(context, type, flags, handle, componentMask, dests, sources);
         }
 
         private static void EmitTexs(
@@ -657,16 +648,7 @@ namespace Ryujinx.Graphics.Shader.Instructions
                 Array.Resize(ref dests, outputIndex);
             }
 
-            TextureOperation operation = context.CreateTextureOperation(
-                Instruction.TextureSample,
-                type,
-                flags,
-                handle,
-                componentMask,
-                dests,
-                sources);
-
-            context.Add(operation);
+            EmitTextureSample(context, type, flags, handle, componentMask, dests, sources);
 
             if (isF16)
             {
@@ -812,18 +794,7 @@ namespace Ryujinx.Graphics.Shader.Instructions
                 Array.Resize(ref dests, outputIndex);
             }
 
-            int handle = imm;
-
-            TextureOperation operation = context.CreateTextureOperation(
-                Instruction.TextureSample,
-                type,
-                flags,
-                handle,
-                componentMask,
-                dests,
-                sources);
-
-            context.Add(operation);
+            EmitTextureSample(context, type, flags, imm, componentMask, dests, sources);
         }
 
         private static void EmitTmml(
@@ -913,15 +884,21 @@ namespace Ryujinx.Graphics.Shader.Instructions
                 return Register(dest++, RegisterType.Gpr);
             }
 
-            int handle = imm;
+            int binding = isBindless ? 0 : context.Config.ResourceManager.GetTextureOrImageBinding(
+                Instruction.Lod,
+                type,
+                TextureFormat.Unknown,
+                flags,
+                TextureOperation.DefaultCbufSlot,
+                imm);
 
             for (int compMask = componentMask, compIndex = 0; compMask != 0; compMask >>= 1, compIndex++)
             {
                 if ((compMask & 1) != 0)
                 {
-                    Operand destOperand = GetDest();
+                    Operand d = GetDest();
 
-                    if (destOperand == null)
+                    if (d == null)
                     {
                         break;
                     }
@@ -930,28 +907,18 @@ namespace Ryujinx.Graphics.Shader.Instructions
                     if (compIndex >= 2)
                     {
                         context.Add(new CommentNode("Unsupported component z or w found"));
-                        context.Copy(destOperand, Const(0));
+                        context.Copy(d, Const(0));
                     }
                     else
                     {
-                        Operand tempDest = Local();
+                        // The instruction component order is the inverse of GLSL's.
+                        Operand res = context.Lod(type, flags, binding, compIndex ^ 1, sources);
 
-                        TextureOperation operation = context.CreateTextureOperation(
-                            Instruction.Lod,
-                            type,
-                            flags,
-                            handle,
-                            compIndex ^ 1, // The instruction component order is the inverse of GLSL's.
-                            new[] { tempDest },
-                            sources);
+                        res = context.FPMultiply(res, ConstF(256.0f));
 
-                        context.Add(operation);
+                        Operand fixedPointValue = context.FP32ConvertToS32(res);
 
-                        tempDest = context.FPMultiply(tempDest, ConstF(256.0f));
-
-                        Operand fixedPointValue = context.FP32ConvertToS32(tempDest);
-
-                        context.Copy(destOperand, fixedPointValue);
+                        context.Copy(d, fixedPointValue);
                     }
                 }
             }
@@ -1081,18 +1048,7 @@ namespace Ryujinx.Graphics.Shader.Instructions
                 Array.Resize(ref dests, outputIndex);
             }
 
-            int handle = imm;
-
-            TextureOperation operation = context.CreateTextureOperation(
-                Instruction.TextureSample,
-                type,
-                flags,
-                handle,
-                componentMask,
-                dests,
-                sources);
-
-            context.Add(operation);
+            EmitTextureSample(context, type, flags, imm, componentMask, dests, sources);
         }
 
         private static void EmitTxq(
@@ -1111,10 +1067,6 @@ namespace Ryujinx.Graphics.Shader.Instructions
 
             context.Config.SetUsedFeature(FeatureFlags.IntegerSampling);
 
-            // TODO: Validate and use query.
-            Instruction inst = Instruction.TextureSize;
-            TextureFlags flags = isBindless ? TextureFlags.Bindless : TextureFlags.None;
-
             Operand Ra()
             {
                 if (srcA > RegisterConsts.RegisterZeroIndex)
@@ -1157,31 +1109,55 @@ namespace Ryujinx.Graphics.Shader.Instructions
                 type = context.Config.GpuAccessor.QuerySamplerType(imm);
             }
 
+            TextureFlags flags = isBindless ? TextureFlags.Bindless : TextureFlags.None;
+
+            int binding = isBindless ? 0 : context.Config.ResourceManager.GetTextureOrImageBinding(
+                Instruction.TextureSize,
+                type,
+                TextureFormat.Unknown,
+                flags,
+                TextureOperation.DefaultCbufSlot,
+                imm);
+
             for (int compMask = componentMask, compIndex = 0; compMask != 0; compMask >>= 1, compIndex++)
             {
                 if ((compMask & 1) != 0)
                 {
-                    Operand destOperand = GetDest();
+                    Operand d = GetDest();
 
-                    if (destOperand == null)
+                    if (d == null)
                     {
                         break;
                     }
 
-                    TextureOperation operation = context.CreateTextureOperation(
-                        inst,
-                        type,
-                        flags,
-                        imm,
-                        compIndex,
-                        new[] { destOperand },
-                        sources);
+                    // TODO: Validate and use query parameter.
+                    Operand res = context.TextureSize(type, flags, binding, compIndex, sources);
 
-                    context.Add(operation);
+                    context.Copy(d, res);
                 }
             }
         }
 
+        private static void EmitTextureSample(
+            EmitterContext context,
+            SamplerType type,
+            TextureFlags flags,
+            int handle,
+            int componentMask,
+            Operand[] dests,
+            Operand[] sources)
+        {
+            int binding = flags.HasFlag(TextureFlags.Bindless) ? 0 : context.Config.ResourceManager.GetTextureOrImageBinding(
+                Instruction.TextureSample,
+                type,
+                TextureFormat.Unknown,
+                flags,
+                TextureOperation.DefaultCbufSlot,
+                handle);
+
+            context.TextureSample(type, flags, binding, componentMask, dests, sources);
+        }
+
         private static SamplerType ConvertSamplerType(TexDim dimensions)
         {
             return dimensions switch
diff --git a/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/TextureOperation.cs b/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/TextureOperation.cs
index b467fe533..fa5550a64 100644
--- a/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/TextureOperation.cs
+++ b/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/TextureOperation.cs
@@ -8,16 +8,14 @@ namespace Ryujinx.Graphics.Shader.IntermediateRepresentation
         public TextureFormat Format { get; set; }
         public TextureFlags Flags { get; private set; }
 
-        public int CbufSlot { get; private set; }
-        public int Handle { get; private set; }
+        public int Binding { get; private set; }
 
         public TextureOperation(
             Instruction inst,
             SamplerType type,
             TextureFormat format,
             TextureFlags flags,
-            int cbufSlot,
-            int handle,
+            int binding,
             int compIndex,
             Operand[] dests,
             Operand[] sources) : base(inst, compIndex, dests, sources)
@@ -25,30 +23,17 @@ namespace Ryujinx.Graphics.Shader.IntermediateRepresentation
             Type = type;
             Format = format;
             Flags = flags;
-            CbufSlot = cbufSlot;
-            Handle = handle;
+            Binding = binding;
         }
 
-        public TextureOperation(
-            Instruction inst,
-            SamplerType type,
-            TextureFormat format,
-            TextureFlags flags,
-            int handle,
-            int compIndex,
-            Operand[] dests,
-            Operand[] sources) : this(inst, type, format, flags, DefaultCbufSlot, handle, compIndex, dests, sources)
-        {
-        }
-
-        public void TurnIntoIndexed(int handle)
+        public void TurnIntoIndexed(int binding)
         {
             Type |= SamplerType.Indexed;
             Flags &= ~TextureFlags.Bindless;
-            Handle = handle;
+            Binding = binding;
         }
 
-        public void SetHandle(int handle, int cbufSlot = DefaultCbufSlot)
+        public void SetBinding(int binding)
         {
             if ((Flags & TextureFlags.Bindless) != 0)
             {
@@ -57,8 +42,7 @@ namespace Ryujinx.Graphics.Shader.IntermediateRepresentation
                 RemoveSource(0);
             }
 
-            CbufSlot = cbufSlot;
-            Handle = handle;
+            Binding = binding;
         }
 
         public void SetLodLevelFlag()
diff --git a/src/Ryujinx.Graphics.Shader/StructuredIr/AstTextureOperation.cs b/src/Ryujinx.Graphics.Shader/StructuredIr/AstTextureOperation.cs
index 4ff2035a8..3970df1e9 100644
--- a/src/Ryujinx.Graphics.Shader/StructuredIr/AstTextureOperation.cs
+++ b/src/Ryujinx.Graphics.Shader/StructuredIr/AstTextureOperation.cs
@@ -8,24 +8,21 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
         public TextureFormat Format { get; }
         public TextureFlags Flags { get; }
 
-        public int CbufSlot { get; }
-        public int Handle { get; }
+        public int Binding { get; }
 
         public AstTextureOperation(
             Instruction inst,
             SamplerType type,
             TextureFormat format,
             TextureFlags flags,
-            int cbufSlot,
-            int handle,
+            int binding,
             int index,
             params IAstNode[] sources) : base(inst, StorageKind.None, false, index, sources, sources.Length)
         {
             Type = type;
             Format = format;
             Flags = flags;
-            CbufSlot = cbufSlot;
-            Handle = handle;
+            Binding = binding;
         }
     }
 }
diff --git a/src/Ryujinx.Graphics.Shader/StructuredIr/ShaderProperties.cs b/src/Ryujinx.Graphics.Shader/StructuredIr/ShaderProperties.cs
index 1da5cb657..048a260ab 100644
--- a/src/Ryujinx.Graphics.Shader/StructuredIr/ShaderProperties.cs
+++ b/src/Ryujinx.Graphics.Shader/StructuredIr/ShaderProperties.cs
@@ -6,11 +6,15 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
     {
         private readonly Dictionary<int, BufferDefinition> _constantBuffers;
         private readonly Dictionary<int, BufferDefinition> _storageBuffers;
+        private readonly Dictionary<int, TextureDefinition> _textures;
+        private readonly Dictionary<int, TextureDefinition> _images;
         private readonly Dictionary<int, MemoryDefinition> _localMemories;
         private readonly Dictionary<int, MemoryDefinition> _sharedMemories;
 
         public IReadOnlyDictionary<int, BufferDefinition> ConstantBuffers => _constantBuffers;
         public IReadOnlyDictionary<int, BufferDefinition> StorageBuffers => _storageBuffers;
+        public IReadOnlyDictionary<int, TextureDefinition> Textures => _textures;
+        public IReadOnlyDictionary<int, TextureDefinition> Images => _images;
         public IReadOnlyDictionary<int, MemoryDefinition> LocalMemories => _localMemories;
         public IReadOnlyDictionary<int, MemoryDefinition> SharedMemories => _sharedMemories;
 
@@ -18,20 +22,32 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
         {
             _constantBuffers = new Dictionary<int, BufferDefinition>();
             _storageBuffers = new Dictionary<int, BufferDefinition>();
+            _textures = new Dictionary<int, TextureDefinition>();
+            _images = new Dictionary<int, TextureDefinition>();
             _localMemories = new Dictionary<int, MemoryDefinition>();
             _sharedMemories = new Dictionary<int, MemoryDefinition>();
         }
 
-        public void AddConstantBuffer(int binding, BufferDefinition definition)
+        public void AddOrUpdateConstantBuffer(int binding, BufferDefinition definition)
         {
             _constantBuffers[binding] = definition;
         }
 
-        public void AddStorageBuffer(int binding, BufferDefinition definition)
+        public void AddOrUpdateStorageBuffer(int binding, BufferDefinition definition)
         {
             _storageBuffers[binding] = definition;
         }
 
+        public void AddOrUpdateTexture(int binding, TextureDefinition descriptor)
+        {
+            _textures[binding] = descriptor;
+        }
+
+        public void AddOrUpdateImage(int binding, TextureDefinition descriptor)
+        {
+            _images[binding] = descriptor;
+        }
+
         public int AddLocalMemory(MemoryDefinition definition)
         {
             int id = _localMemories.Count;
diff --git a/src/Ryujinx.Graphics.Shader/StructuredIr/StructuredProgram.cs b/src/Ryujinx.Graphics.Shader/StructuredIr/StructuredProgram.cs
index a4e6444b0..87acedf62 100644
--- a/src/Ryujinx.Graphics.Shader/StructuredIr/StructuredProgram.cs
+++ b/src/Ryujinx.Graphics.Shader/StructuredIr/StructuredProgram.cs
@@ -125,15 +125,7 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
 
             AstTextureOperation GetAstTextureOperation(TextureOperation texOp)
             {
-                return new AstTextureOperation(
-                    inst,
-                    texOp.Type,
-                    texOp.Format,
-                    texOp.Flags,
-                    texOp.CbufSlot,
-                    texOp.Handle,
-                    texOp.Index,
-                    sources);
+                return new AstTextureOperation(inst, texOp.Type, texOp.Format, texOp.Flags, texOp.Binding, texOp.Index, sources);
             }
 
             int componentsCount = BitOperations.PopCount((uint)operation.Index);
diff --git a/src/Ryujinx.Graphics.Shader/StructuredIr/TextureDefinition.cs b/src/Ryujinx.Graphics.Shader/StructuredIr/TextureDefinition.cs
new file mode 100644
index 000000000..ba31dae6c
--- /dev/null
+++ b/src/Ryujinx.Graphics.Shader/StructuredIr/TextureDefinition.cs
@@ -0,0 +1,27 @@
+namespace Ryujinx.Graphics.Shader
+{
+    readonly struct TextureDefinition
+    {
+        public int Set { get; }
+        public int Binding { get; }
+        public string Name { get; }
+        public SamplerType Type { get; }
+        public TextureFormat Format { get; }
+        public TextureUsageFlags Flags { get; }
+
+        public TextureDefinition(int set, int binding, string name, SamplerType type, TextureFormat format, TextureUsageFlags flags)
+        {
+            Set = set;
+            Binding = binding;
+            Name = name;
+            Type = type;
+            Format = format;
+            Flags = flags;
+        }
+
+        public TextureDefinition SetFlag(TextureUsageFlags flag)
+        {
+            return new TextureDefinition(Set, Binding, Name, Type, Format, Flags | flag);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Ryujinx.Graphics.Shader/TextureDescriptor.cs b/src/Ryujinx.Graphics.Shader/TextureDescriptor.cs
index 626faa695..58b8e5a51 100644
--- a/src/Ryujinx.Graphics.Shader/TextureDescriptor.cs
+++ b/src/Ryujinx.Graphics.Shader/TextureDescriptor.cs
@@ -12,23 +12,16 @@ namespace Ryujinx.Graphics.Shader
         public readonly int CbufSlot;
         public readonly int HandleIndex;
 
-        public TextureUsageFlags Flags;
+        public readonly TextureUsageFlags Flags;
 
-        public TextureDescriptor(int binding, SamplerType type, TextureFormat format, int cbufSlot, int handleIndex)
+        public TextureDescriptor(int binding, SamplerType type, TextureFormat format, int cbufSlot, int handleIndex, TextureUsageFlags flags)
         {
             Binding = binding;
             Type = type;
             Format = format;
             CbufSlot = cbufSlot;
             HandleIndex = handleIndex;
-            Flags = TextureUsageFlags.None;
-        }
-
-        public TextureDescriptor SetFlag(TextureUsageFlags flag)
-        {
-            Flags |= flag;
-
-            return this;
+            Flags = flags;
         }
     }
 }
diff --git a/src/Ryujinx.Graphics.Shader/Translation/EmitterContext.cs b/src/Ryujinx.Graphics.Shader/Translation/EmitterContext.cs
index 9eedc3f91..08d4b9156 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/EmitterContext.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/EmitterContext.cs
@@ -115,36 +115,6 @@ namespace Ryujinx.Graphics.Shader.Translation
             _operations.Add(operation);
         }
 
-        public TextureOperation CreateTextureOperation(
-            Instruction inst,
-            SamplerType type,
-            TextureFlags flags,
-            int handle,
-            int compIndex,
-            Operand[] dests,
-            params Operand[] sources)
-        {
-            return CreateTextureOperation(inst, type, TextureFormat.Unknown, flags, handle, compIndex, dests, sources);
-        }
-
-        public TextureOperation CreateTextureOperation(
-            Instruction inst,
-            SamplerType type,
-            TextureFormat format,
-            TextureFlags flags,
-            int handle,
-            int compIndex,
-            Operand[] dests,
-            params Operand[] sources)
-        {
-            if (!flags.HasFlag(TextureFlags.Bindless))
-            {
-                Config.SetUsedTexture(inst, type, format, flags, TextureOperation.DefaultCbufSlot, handle);
-            }
-
-            return new TextureOperation(inst, type, format, flags, handle, compIndex, dests, sources);
-        }
-
         public void FlagAttributeRead(int attribute)
         {
             if (Config.Stage == ShaderStage.Vertex && attribute == AttributeConsts.InstanceId)
diff --git a/src/Ryujinx.Graphics.Shader/Translation/EmitterContextInsts.cs b/src/Ryujinx.Graphics.Shader/Translation/EmitterContextInsts.cs
index c2f1b790f..c92d05838 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/EmitterContextInsts.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/EmitterContextInsts.cs
@@ -604,6 +604,45 @@ namespace Ryujinx.Graphics.Shader.Translation
             return context.Add(Instruction.Subtract, Local(), a, b);
         }
 
+        public static Operand ImageAtomic(
+            this EmitterContext context,
+            SamplerType type,
+            TextureFormat format,
+            TextureFlags flags,
+            int binding,
+            Operand[] sources)
+        {
+            Operand dest = Local();
+
+            context.Add(new TextureOperation(Instruction.ImageAtomic, type, format, flags, binding, 0, new[] { dest }, sources));
+
+            return dest;
+        }
+
+        public static void ImageLoad(
+            this EmitterContext context,
+            SamplerType type,
+            TextureFormat format,
+            TextureFlags flags,
+            int binding,
+            int compMask,
+            Operand[] dests,
+            Operand[] sources)
+        {
+            context.Add(new TextureOperation(Instruction.ImageLoad, type, format, flags, binding, compMask, dests, sources));
+        }
+
+        public static void ImageStore(
+            this EmitterContext context,
+            SamplerType type,
+            TextureFormat format,
+            TextureFlags flags,
+            int binding,
+            Operand[] sources)
+        {
+            context.Add(new TextureOperation(Instruction.ImageStore, type, format, flags, binding, 0, null, sources));
+        }
+
         public static Operand IsNan(this EmitterContext context, Operand a, Instruction fpType = Instruction.FP32)
         {
             return context.Add(fpType | Instruction.IsNan, Local(), a);
@@ -666,6 +705,21 @@ namespace Ryujinx.Graphics.Shader.Translation
                 : context.Load(storageKind, (int)ioVariable, arrayIndex, elemIndex);
         }
 
+        public static Operand Lod(
+            this EmitterContext context,
+            SamplerType type,
+            TextureFlags flags,
+            int binding,
+            int compIndex,
+            Operand[] sources)
+        {
+            Operand dest = Local();
+
+            context.Add(new TextureOperation(Instruction.Lod, type, TextureFormat.Unknown, flags, binding, compIndex, new[] { dest }, sources));
+
+            return dest;
+        }
+
         public static Operand MemoryBarrier(this EmitterContext context)
         {
             return context.Add(Instruction.MemoryBarrier);
@@ -797,6 +851,33 @@ namespace Ryujinx.Graphics.Shader.Translation
                 : context.Add(Instruction.Store, storageKind, null, Const((int)ioVariable), arrayIndex, elemIndex, value);
         }
 
+        public static void TextureSample(
+            this EmitterContext context,
+            SamplerType type,
+            TextureFlags flags,
+            int binding,
+            int compMask,
+            Operand[] dests,
+            Operand[] sources)
+        {
+            context.Add(new TextureOperation(Instruction.TextureSample, type, TextureFormat.Unknown, flags, binding, compMask, dests, sources));
+        }
+
+        public static Operand TextureSize(
+            this EmitterContext context,
+            SamplerType type,
+            TextureFlags flags,
+            int binding,
+            int compIndex,
+            Operand[] sources)
+        {
+            Operand dest = Local();
+
+            context.Add(new TextureOperation(Instruction.TextureSize, type, TextureFormat.Unknown, flags, binding, compIndex, new[] { dest }, sources));
+
+            return dest;
+        }
+
         public static Operand UnpackDouble2x32High(this EmitterContext context, Operand a)
         {
             return UnpackDouble2x32(context, a, 1);
diff --git a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessElimination.cs b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessElimination.cs
index bb25c1602..bf087affb 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessElimination.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessElimination.cs
@@ -222,8 +222,6 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
 
         private static void SetHandle(ShaderConfig config, TextureOperation texOp, int cbufOffset, int cbufSlot, bool rewriteSamplerType, bool isImage)
         {
-            texOp.SetHandle(cbufOffset, cbufSlot);
-
             if (rewriteSamplerType)
             {
                 SamplerType newType = config.GpuAccessor.QuerySamplerType(cbufOffset, cbufSlot);
@@ -234,7 +232,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
                 }
                 else if (texOp.Type == SamplerType.TextureBuffer && newType == SamplerType.Texture1D)
                 {
-                    int coordsCount = 1;
+                    int coordsCount = 2;
 
                     if (InstEmit.Sample1DAs2D)
                     {
@@ -255,7 +253,15 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
                 }
             }
 
-            config.SetUsedTexture(texOp.Inst, texOp.Type, texOp.Format, texOp.Flags, cbufSlot, cbufOffset);
+            int binding = config.ResourceManager.GetTextureOrImageBinding(
+                texOp.Inst,
+                texOp.Type,
+                texOp.Format,
+                texOp.Flags & ~TextureFlags.Bindless,
+                cbufSlot,
+                cbufOffset);
+
+            texOp.SetBinding(binding);
         }
     }
 }
diff --git a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessToIndexed.cs b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessToIndexed.cs
index f966a4fc5..4b1bf76e5 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessToIndexed.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessToIndexed.cs
@@ -7,6 +7,8 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
 {
     static class BindlessToIndexed
     {
+        private const int NvnTextureBufferIndex = 2;
+
         public static void RunPass(BasicBlock block, ShaderConfig config)
         {
             // We can turn a bindless texture access into a indexed access,
@@ -43,7 +45,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
 
                 if (ldcSrc0.Type != OperandType.Constant ||
                     !config.ResourceManager.TryGetConstantBufferSlot(ldcSrc0.Value, out int src0CbufSlot) ||
-                    src0CbufSlot != 2)
+                    src0CbufSlot != NvnTextureBufferIndex)
                 {
                     continue;
                 }
@@ -102,8 +104,15 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
 
         private static void TurnIntoIndexed(ShaderConfig config, TextureOperation texOp, int handle)
         {
-            texOp.TurnIntoIndexed(handle);
-            config.SetUsedTexture(texOp.Inst, texOp.Type, texOp.Format, texOp.Flags, texOp.CbufSlot, handle);
+            int binding = config.ResourceManager.GetTextureOrImageBinding(
+                texOp.Inst,
+                texOp.Type | SamplerType.Indexed,
+                texOp.Format,
+                texOp.Flags & ~TextureFlags.Bindless,
+                NvnTextureBufferIndex,
+                handle);
+
+            texOp.TurnIntoIndexed(binding);
         }
     }
 }
diff --git a/src/Ryujinx.Graphics.Shader/Translation/ResourceManager.cs b/src/Ryujinx.Graphics.Shader/Translation/ResourceManager.cs
index f3a5ba6c4..5991cd25d 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/ResourceManager.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/ResourceManager.cs
@@ -1,4 +1,5 @@
 using Ryujinx.Common;
+using Ryujinx.Graphics.Shader.IntermediateRepresentation;
 using Ryujinx.Graphics.Shader.StructuredIr;
 using System;
 using System.Collections.Generic;
@@ -13,9 +14,13 @@ namespace Ryujinx.Graphics.Shader.Translation
         private const int DefaultLocalMemorySize = 128;
         private const int DefaultSharedMemorySize = 4096;
 
-        private static readonly string[] _stagePrefixes = { "cp", "vp", "tcp", "tep", "gp", "fp" };
+        // TODO: Non-hardcoded array size.
+        public const int SamplerArraySize = 4;
+
+        private static readonly string[] _stagePrefixes = new string[] { "cp", "vp", "tcp", "tep", "gp", "fp" };
 
         private readonly IGpuAccessor _gpuAccessor;
+        private readonly ShaderStage _stage;
         private readonly string _stagePrefix;
 
         private readonly int[] _cbSlotToBindingMap;
@@ -27,6 +32,19 @@ namespace Ryujinx.Graphics.Shader.Translation
 
         private readonly HashSet<int> _usedConstantBufferBindings;
 
+        private readonly record struct TextureInfo(int CbufSlot, int Handle, bool Indexed, TextureFormat Format);
+
+        private struct TextureMeta
+        {
+            public int Binding;
+            public bool AccurateType;
+            public SamplerType Type;
+            public TextureUsageFlags UsageFlags;
+        }
+
+        private readonly Dictionary<TextureInfo, TextureMeta> _usedTextures;
+        private readonly Dictionary<TextureInfo, TextureMeta> _usedImages;
+
         public int LocalMemoryId { get; private set; }
         public int SharedMemoryId { get; private set; }
 
@@ -36,6 +54,7 @@ namespace Ryujinx.Graphics.Shader.Translation
         {
             _gpuAccessor = gpuAccessor;
             Properties = properties;
+            _stage = stage;
             _stagePrefix = GetShaderStagePrefix(stage);
 
             _cbSlotToBindingMap = new int[18];
@@ -48,7 +67,10 @@ namespace Ryujinx.Graphics.Shader.Translation
 
             _usedConstantBufferBindings = new HashSet<int>();
 
-            properties.AddConstantBuffer(0, new BufferDefinition(BufferLayout.Std140, 0, 0, "support_buffer", SupportBuffer.GetStructureType()));
+            _usedTextures = new Dictionary<TextureInfo, TextureMeta>();
+            _usedImages = new Dictionary<TextureInfo, TextureMeta>();
+
+            properties.AddOrUpdateConstantBuffer(0, new BufferDefinition(BufferLayout.Std140, 0, 0, "support_buffer", SupportBuffer.GetStructureType()));
 
             LocalMemoryId = -1;
             SharedMemoryId = -1;
@@ -166,6 +188,198 @@ namespace Ryujinx.Graphics.Shader.Translation
             return false;
         }
 
+        public int GetTextureOrImageBinding(
+            Instruction inst,
+            SamplerType type,
+            TextureFormat format,
+            TextureFlags flags,
+            int cbufSlot,
+            int handle)
+        {
+            inst &= Instruction.Mask;
+            bool isImage = inst == Instruction.ImageLoad || inst == Instruction.ImageStore || inst == Instruction.ImageAtomic;
+            bool isWrite = inst == Instruction.ImageStore || inst == Instruction.ImageAtomic;
+            bool accurateType = inst != Instruction.Lod && inst != Instruction.TextureSize;
+            bool intCoords = isImage || flags.HasFlag(TextureFlags.IntCoords) || inst == Instruction.TextureSize;
+            bool coherent = flags.HasFlag(TextureFlags.Coherent);
+
+            if (!isImage)
+            {
+                format = TextureFormat.Unknown;
+            }
+
+            int binding = GetTextureOrImageBinding(cbufSlot, handle, type, format, isImage, intCoords, isWrite, accurateType, coherent);
+
+            _gpuAccessor.RegisterTexture(handle, cbufSlot);
+
+            return binding;
+        }
+
+        private int GetTextureOrImageBinding(
+            int cbufSlot,
+            int handle,
+            SamplerType type,
+            TextureFormat format,
+            bool isImage,
+            bool intCoords,
+            bool write,
+            bool accurateType,
+            bool coherent)
+        {
+            var dimensions = type.GetDimensions();
+            var isIndexed = type.HasFlag(SamplerType.Indexed);
+            var dict = isImage ? _usedImages : _usedTextures;
+
+            var usageFlags = TextureUsageFlags.None;
+
+            if (intCoords)
+            {
+                usageFlags |= TextureUsageFlags.NeedsScaleValue;
+
+                var canScale = _stage.SupportsRenderScale() && !isIndexed && !write && dimensions == 2;
+
+                if (!canScale)
+                {
+                    // Resolution scaling cannot be applied to this texture right now.
+                    // Flag so that we know to blacklist scaling on related textures when binding them.
+                    usageFlags |= TextureUsageFlags.ResScaleUnsupported;
+                }
+            }
+
+            if (write)
+            {
+                usageFlags |= TextureUsageFlags.ImageStore;
+            }
+
+            if (coherent)
+            {
+                usageFlags |= TextureUsageFlags.ImageCoherent;
+            }
+
+            int arraySize = isIndexed ? SamplerArraySize : 1;
+            int firstBinding = -1;
+
+            for (int layer = 0; layer < arraySize; layer++)
+            {
+                var info = new TextureInfo(cbufSlot, handle + layer * 2, isIndexed, format);
+                var meta = new TextureMeta()
+                {
+                    AccurateType = accurateType,
+                    Type = type,
+                    UsageFlags = usageFlags
+                };
+
+                int binding;
+
+                if (dict.TryGetValue(info, out var existingMeta))
+                {
+                    dict[info] = MergeTextureMeta(meta, existingMeta);
+                    binding = existingMeta.Binding;
+                }
+                else
+                {
+                    bool isBuffer = (type & SamplerType.Mask) == SamplerType.TextureBuffer;
+
+                    binding = isImage
+                        ? _gpuAccessor.QueryBindingImage(dict.Count, isBuffer)
+                        : _gpuAccessor.QueryBindingTexture(dict.Count, isBuffer);
+
+                    meta.Binding = binding;
+
+                    dict.Add(info, meta);
+                }
+
+                string nameSuffix;
+
+                if (isImage)
+                {
+                    nameSuffix = cbufSlot < 0
+                        ? $"i_tcb_{handle:X}_{format.ToGlslFormat()}"
+                        : $"i_cb{cbufSlot}_{handle:X}_{format.ToGlslFormat()}";
+                }
+                else
+                {
+                    nameSuffix = cbufSlot < 0 ? $"t_tcb_{handle:X}" : $"t_cb{cbufSlot}_{handle:X}";
+                }
+
+                var definition = new TextureDefinition(
+                    isImage ? 3 : 2,
+                    binding,
+                    $"{_stagePrefix}_{nameSuffix}",
+                    meta.Type,
+                    info.Format,
+                    meta.UsageFlags);
+
+                if (isImage)
+                {
+                    Properties.AddOrUpdateImage(binding, definition);
+                }
+                else
+                {
+                    Properties.AddOrUpdateTexture(binding, definition);
+                }
+
+                if (layer == 0)
+                {
+                    firstBinding = binding;
+                }
+            }
+
+            return firstBinding;
+        }
+
+        private static TextureMeta MergeTextureMeta(TextureMeta meta, TextureMeta existingMeta)
+        {
+            meta.Binding = existingMeta.Binding;
+            meta.UsageFlags |= existingMeta.UsageFlags;
+
+            // If the texture we have has inaccurate type information, then
+            // we prefer the most accurate one.
+            if (existingMeta.AccurateType)
+            {
+                meta.AccurateType = true;
+                meta.Type = existingMeta.Type;
+            }
+
+            return meta;
+        }
+
+        public void SetUsageFlagsForTextureQuery(int binding, SamplerType type)
+        {
+            TextureInfo selectedInfo = default;
+            TextureMeta selectedMeta = default;
+            bool found = false;
+
+            foreach ((TextureInfo info, TextureMeta meta) in _usedTextures)
+            {
+                if (meta.Binding == binding)
+                {
+                    selectedInfo = info;
+                    selectedMeta = meta;
+                    found = true;
+                    break;
+                }
+            }
+
+            if (found)
+            {
+                selectedMeta.UsageFlags |= TextureUsageFlags.NeedsScaleValue;
+
+                var dimensions = type.GetDimensions();
+                var isIndexed = type.HasFlag(SamplerType.Indexed);
+                var canScale = _stage.SupportsRenderScale() && !isIndexed && dimensions == 2;
+
+                if (!canScale)
+                {
+                    // Resolution scaling cannot be applied to this texture right now.
+                    // Flag so that we know to blacklist scaling on related textures when binding them.
+                    selectedMeta.UsageFlags |= TextureUsageFlags.ResScaleUnsupported;
+                }
+
+                _usedTextures[selectedInfo] = selectedMeta;
+            }
+        }
+
         public void SetUsedConstantBufferBinding(int binding)
         {
             _usedConstantBufferBindings.Add(binding);
@@ -208,10 +422,8 @@ namespace Ryujinx.Graphics.Shader.Translation
                 if (binding >= 0)
                 {
                     (int sbCbSlot, int sbCbOffset) = UnpackSbCbInfo(key);
-                    descriptors[descriptorIndex++] = new BufferDescriptor(binding, slot, sbCbSlot, sbCbOffset)
-                    {
-                        Flags = (_sbSlotWritten & (1u << slot)) != 0 ? BufferUsageFlags.Write : BufferUsageFlags.None,
-                    };
+                    BufferUsageFlags flags = (_sbSlotWritten & (1u << slot)) != 0 ? BufferUsageFlags.Write : BufferUsageFlags.None;
+                    descriptors[descriptorIndex++] = new BufferDescriptor(binding, slot, sbCbSlot, sbCbOffset, flags);
                 }
             }
 
@@ -223,6 +435,64 @@ namespace Ryujinx.Graphics.Shader.Translation
             return descriptors;
         }
 
+        public TextureDescriptor[] GetTextureDescriptors()
+        {
+            return GetDescriptors(_usedTextures, _usedTextures.Count);
+        }
+
+        public TextureDescriptor[] GetImageDescriptors()
+        {
+            return GetDescriptors(_usedImages, _usedImages.Count);
+        }
+
+        private static TextureDescriptor[] GetDescriptors(IReadOnlyDictionary<TextureInfo, TextureMeta> usedResources, int count)
+        {
+            TextureDescriptor[] descriptors = new TextureDescriptor[count];
+
+            int descriptorIndex = 0;
+
+            foreach ((TextureInfo info, TextureMeta meta) in usedResources)
+            {
+                descriptors[descriptorIndex++] = new TextureDescriptor(
+                    meta.Binding,
+                    meta.Type,
+                    info.Format,
+                    info.CbufSlot,
+                    info.Handle,
+                    meta.UsageFlags);
+            }
+
+            return descriptors;
+        }
+
+        public (int, int) GetCbufSlotAndHandleForTexture(int binding)
+        {
+            foreach ((TextureInfo info, TextureMeta meta) in _usedTextures)
+            {
+                if (meta.Binding == binding)
+                {
+                    return (info.CbufSlot, info.Handle);
+                }
+            }
+
+            throw new ArgumentException($"Binding {binding} is invalid.");
+        }
+
+        private static int FindDescriptorIndex(TextureDescriptor[] array, int binding)
+        {
+            return Array.FindIndex(array, x => x.Binding == binding);
+        }
+
+        public int FindTextureDescriptorIndex(int binding)
+        {
+            return FindDescriptorIndex(GetTextureDescriptors(), binding);
+        }
+
+        public int FindImageDescriptorIndex(int binding)
+        {
+            return FindDescriptorIndex(GetImageDescriptors(), binding);
+        }
+
         private void AddNewConstantBuffer(int binding, string name)
         {
             StructureType type = new(new[]
@@ -230,7 +500,7 @@ namespace Ryujinx.Graphics.Shader.Translation
                 new StructureField(AggregateType.Array | AggregateType.Vector4 | AggregateType.FP32, "data", Constants.ConstantBufferSize / 16),
             });
 
-            Properties.AddConstantBuffer(binding, new BufferDefinition(BufferLayout.Std140, 0, binding, name, type));
+            Properties.AddOrUpdateConstantBuffer(binding, new BufferDefinition(BufferLayout.Std140, 0, binding, name, type));
         }
 
         private void AddNewStorageBuffer(int binding, string name)
@@ -240,7 +510,7 @@ namespace Ryujinx.Graphics.Shader.Translation
                 new StructureField(AggregateType.Array | AggregateType.U32, "data", 0),
             });
 
-            Properties.AddStorageBuffer(binding, new BufferDefinition(BufferLayout.Std430, 1, binding, name, type));
+            Properties.AddOrUpdateStorageBuffer(binding, new BufferDefinition(BufferLayout.Std430, 1, binding, name, type));
         }
 
         public static string GetShaderStagePrefix(ShaderStage stage)
diff --git a/src/Ryujinx.Graphics.Shader/Translation/Rewriter.cs b/src/Ryujinx.Graphics.Shader/Translation/Rewriter.cs
index 42e3ecee3..0fa75203b 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/Rewriter.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/Rewriter.cs
@@ -61,7 +61,7 @@ namespace Ryujinx.Graphics.Shader.Translation
 
                         if (texOp.Inst == Instruction.TextureSample)
                         {
-                            node = InsertCoordNormalization(node, config);
+                            node = InsertCoordNormalization(hfm, node, config);
                             node = InsertCoordGatherBias(node, config);
                             node = InsertConstOffsets(node, config);
 
@@ -285,8 +285,8 @@ namespace Ryujinx.Graphics.Shader.Translation
             {
                 int functionId = hfm.GetOrCreateFunctionId(HelperFunctionName.TexelFetchScale);
                 int samplerIndex = isImage
-                    ? config.GetTextureDescriptors().Length + config.FindImageDescriptorIndex(texOp)
-                    : config.FindTextureDescriptorIndex(texOp);
+                    ? config.ResourceManager.GetTextureDescriptors().Length + config.ResourceManager.FindImageDescriptorIndex(texOp.Binding)
+                    : config.ResourceManager.FindTextureDescriptorIndex(texOp.Binding);
 
                 for (int index = 0; index < coordsCount; index++)
                 {
@@ -326,7 +326,7 @@ namespace Ryujinx.Graphics.Shader.Translation
                 TypeSupportsScale(texOp.Type))
             {
                 int functionId = hfm.GetOrCreateFunctionId(HelperFunctionName.TextureSizeUnscale);
-                int samplerIndex = config.FindTextureDescriptorIndex(texOp, ignoreType: true);
+                int samplerIndex = config.ResourceManager.FindTextureDescriptorIndex(texOp.Binding);
 
                 for (int index = texOp.DestsCount - 1; index >= 0; index--)
                 {
@@ -368,7 +368,7 @@ namespace Ryujinx.Graphics.Shader.Translation
             return (type & SamplerType.Mask) == SamplerType.Texture2D;
         }
 
-        private static LinkedListNode<INode> InsertCoordNormalization(LinkedListNode<INode> node, ShaderConfig config)
+        private static LinkedListNode<INode> InsertCoordNormalization(HelperFunctionManager hfm, LinkedListNode<INode> node, ShaderConfig config)
         {
             // Emulate non-normalized coordinates by normalizing the coordinates on the shader.
             // Without normalization, the coordinates are expected to the in the [0, W or H] range,
@@ -378,9 +378,17 @@ namespace Ryujinx.Graphics.Shader.Translation
             TextureOperation texOp = (TextureOperation)node.Value;
 
             bool isBindless = (texOp.Flags & TextureFlags.Bindless) != 0;
+
+            if (isBindless)
+            {
+                return node;
+            }
+
             bool intCoords = (texOp.Flags & TextureFlags.IntCoords) != 0;
 
-            bool isCoordNormalized = isBindless || config.GpuAccessor.QueryTextureCoordNormalized(texOp.Handle, texOp.CbufSlot);
+            (int cbufSlot, int handle) = config.ResourceManager.GetCbufSlotAndHandleForTexture(texOp.Binding);
+
+            bool isCoordNormalized = config.GpuAccessor.QueryTextureCoordNormalized(handle, cbufSlot);
 
             if (isCoordNormalized || intCoords)
             {
@@ -411,18 +419,17 @@ namespace Ryujinx.Graphics.Shader.Translation
                     texSizeSources = new Operand[] { Const(0) };
                 }
 
-                node.List.AddBefore(node, new TextureOperation(
+                LinkedListNode<INode> textureSizeNode = node.List.AddBefore(node, new TextureOperation(
                     Instruction.TextureSize,
                     texOp.Type,
                     texOp.Format,
                     texOp.Flags,
-                    texOp.CbufSlot,
-                    texOp.Handle,
+                    texOp.Binding,
                     index,
                     new[] { coordSize },
                     texSizeSources));
 
-                config.SetUsedTexture(Instruction.TextureSize, texOp.Type, texOp.Format, texOp.Flags, texOp.CbufSlot, texOp.Handle);
+                config.ResourceManager.SetUsageFlagsForTextureQuery(texOp.Binding, texOp.Type);
 
                 Operand source = texOp.GetSource(coordsIndex + index);
 
@@ -431,6 +438,8 @@ namespace Ryujinx.Graphics.Shader.Translation
                 node.List.AddBefore(node, new Operation(Instruction.FP32 | Instruction.Divide, coordNormalized, source, GenerateI2f(node, coordSize)));
 
                 texOp.SetSource(coordsIndex + index, coordNormalized);
+
+                InsertTextureSizeUnscale(hfm, textureSizeNode, config);
             }
 
             return node;
@@ -491,14 +500,11 @@ namespace Ryujinx.Graphics.Shader.Translation
                     texOp.Type,
                     texOp.Format,
                     texOp.Flags,
-                    texOp.CbufSlot,
-                    texOp.Handle,
+                    texOp.Binding,
                     index,
                     new[] { coordSize },
                     texSizeSources));
 
-                config.SetUsedTexture(Instruction.TextureSize, texOp.Type, texOp.Format, texOp.Flags, texOp.CbufSlot, texOp.Handle);
-
                 node.List.AddBefore(node, new Operation(
                     Instruction.FP32 | Instruction.Multiply,
                     scaledSize,
@@ -686,8 +692,6 @@ namespace Ryujinx.Graphics.Shader.Translation
 
                     for (int index = 0; index < coordsCount; index++)
                     {
-                        config.SetUsedTexture(Instruction.TextureSize, texOp.Type, texOp.Format, texOp.Flags, texOp.CbufSlot, texOp.Handle);
-
                         Operand offset = Local();
 
                         Operand intOffset = offsets[index + (hasOffsets ? compIndex * coordsCount : 0)];
@@ -712,8 +716,7 @@ namespace Ryujinx.Graphics.Shader.Translation
                         texOp.Type,
                         texOp.Format,
                         texOp.Flags & ~(TextureFlags.Offset | TextureFlags.Offsets),
-                        texOp.CbufSlot,
-                        texOp.Handle,
+                        texOp.Binding,
                         1,
                         new[] { dests[destIndex++] },
                         newSources);
@@ -744,8 +747,6 @@ namespace Ryujinx.Graphics.Shader.Translation
 
                     for (int index = 0; index < coordsCount; index++)
                     {
-                        config.SetUsedTexture(Instruction.TextureSize, texOp.Type, texOp.Format, texOp.Flags, texOp.CbufSlot, texOp.Handle);
-
                         Operand offset = Local();
 
                         Operand intOffset = offsets[index];
@@ -771,8 +772,7 @@ namespace Ryujinx.Graphics.Shader.Translation
                     texOp.Type,
                     texOp.Format,
                     texOp.Flags & ~(TextureFlags.Offset | TextureFlags.Offsets),
-                    texOp.CbufSlot,
-                    texOp.Handle,
+                    texOp.Binding,
                     componentIndex,
                     dests,
                     sources);
@@ -806,8 +806,7 @@ namespace Ryujinx.Graphics.Shader.Translation
                 texOp.Type,
                 texOp.Format,
                 texOp.Flags,
-                texOp.CbufSlot,
-                texOp.Handle,
+                texOp.Binding,
                 0,
                 new[] { lod },
                 lodSources));
@@ -832,8 +831,7 @@ namespace Ryujinx.Graphics.Shader.Translation
                     texOp.Type,
                     texOp.Format,
                     texOp.Flags,
-                    texOp.CbufSlot,
-                    texOp.Handle,
+                    texOp.Binding,
                     index,
                     new[] { texSizes[index] },
                     texSizeSources));
@@ -853,7 +851,9 @@ namespace Ryujinx.Graphics.Shader.Translation
                 return node;
             }
 
-            TextureFormat format = config.GpuAccessor.QueryTextureFormat(texOp.Handle, texOp.CbufSlot);
+            (int cbufSlot, int handle) = config.ResourceManager.GetCbufSlotAndHandleForTexture(texOp.Binding);
+
+            TextureFormat format = config.GpuAccessor.QueryTextureFormat(handle, cbufSlot);
 
             int maxPositive = format switch
             {
diff --git a/src/Ryujinx.Graphics.Shader/Translation/ShaderConfig.cs b/src/Ryujinx.Graphics.Shader/Translation/ShaderConfig.cs
index e93a709c2..5741d0288 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/ShaderConfig.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/ShaderConfig.cs
@@ -2,16 +2,12 @@ using Ryujinx.Graphics.Shader.IntermediateRepresentation;
 using Ryujinx.Graphics.Shader.StructuredIr;
 using System;
 using System.Collections.Generic;
-using System.Linq;
 using System.Numerics;
 
 namespace Ryujinx.Graphics.Shader.Translation
 {
     class ShaderConfig
     {
-        // TODO: Non-hardcoded array size.
-        public const int SamplerArraySize = 4;
-
         private const int ThreadsPerWarp = 32;
 
         public ShaderStage Stage { get; }
@@ -110,20 +106,6 @@ namespace Ryujinx.Graphics.Shader.Translation
         public UInt128 NextInputAttributesComponents { get; private set; }
         public UInt128 ThisInputAttributesComponents { get; private set; }
 
-        private readonly record struct TextureInfo(int CbufSlot, int Handle, bool Indexed, TextureFormat Format);
-
-        private struct TextureMeta
-        {
-            public bool AccurateType;
-            public SamplerType Type;
-            public TextureUsageFlags UsageFlags;
-        }
-
-        private readonly Dictionary<TextureInfo, TextureMeta> _usedTextures;
-        private readonly Dictionary<TextureInfo, TextureMeta> _usedImages;
-        private TextureDescriptor[] _cachedTextureDescriptors;
-        private TextureDescriptor[] _cachedImageDescriptors;
-
         public ShaderConfig(ShaderStage stage, IGpuAccessor gpuAccessor, TranslationOptions options, int localMemorySize)
         {
             Stage = stage;
@@ -141,9 +123,6 @@ namespace Ryujinx.Graphics.Shader.Translation
             UsedInputAttributesPerPatch = new HashSet<int>();
             UsedOutputAttributesPerPatch = new HashSet<int>();
 
-            _usedTextures = new Dictionary<TextureInfo, TextureMeta>();
-            _usedImages = new Dictionary<TextureInfo, TextureMeta>();
-
             ResourceManager = new ResourceManager(stage, gpuAccessor, new ShaderProperties());
 
             if (!gpuAccessor.QueryHostSupportsTransformFeedback() && gpuAccessor.QueryTransformFeedbackEnabled())
@@ -156,7 +135,7 @@ namespace Ryujinx.Graphics.Shader.Translation
 
                 BufferDefinition tfeInfoBuffer = new(BufferLayout.Std430, 1, Constants.TfeInfoBinding, "tfe_info", tfeInfoStruct);
 
-                Properties.AddStorageBuffer(Constants.TfeInfoBinding, tfeInfoBuffer);
+                Properties.AddOrUpdateStorageBuffer(Constants.TfeInfoBinding, tfeInfoBuffer);
 
                 StructureType tfeDataStruct = new(new StructureField[]
                 {
@@ -167,7 +146,7 @@ namespace Ryujinx.Graphics.Shader.Translation
                 {
                     int binding = Constants.TfeBufferBaseBinding + i;
                     BufferDefinition tfeDataBuffer = new(BufferLayout.Std430, 1, binding, $"tfe_data{i}", tfeDataStruct);
-                    Properties.AddStorageBuffer(binding, tfeDataBuffer);
+                    Properties.AddOrUpdateStorageBuffer(binding, tfeDataBuffer);
                 }
             }
         }
@@ -443,22 +422,6 @@ namespace Ryujinx.Graphics.Shader.Translation
 
             UsedInputAttributes |= other.UsedInputAttributes;
             UsedOutputAttributes |= other.UsedOutputAttributes;
-
-            foreach (var kv in other._usedTextures)
-            {
-                if (!_usedTextures.TryAdd(kv.Key, kv.Value))
-                {
-                    _usedTextures[kv.Key] = MergeTextureMeta(kv.Value, _usedTextures[kv.Key]);
-                }
-            }
-
-            foreach (var kv in other._usedImages)
-            {
-                if (!_usedImages.TryAdd(kv.Key, kv.Value))
-                {
-                    _usedImages[kv.Key] = MergeTextureMeta(kv.Value, _usedImages[kv.Key]);
-                }
-            }
         }
 
         public void SetLayerOutputAttribute(int attr)
@@ -642,196 +605,13 @@ namespace Ryujinx.Graphics.Shader.Translation
             UsedFeatures |= flags;
         }
 
-        public void SetUsedTexture(
-            Instruction inst,
-            SamplerType type,
-            TextureFormat format,
-            TextureFlags flags,
-            int cbufSlot,
-            int handle)
-        {
-            inst &= Instruction.Mask;
-            bool isImage = inst == Instruction.ImageLoad || inst == Instruction.ImageStore || inst == Instruction.ImageAtomic;
-            bool isWrite = inst == Instruction.ImageStore || inst == Instruction.ImageAtomic;
-            bool accurateType = inst != Instruction.Lod && inst != Instruction.TextureSize;
-            bool coherent = flags.HasFlag(TextureFlags.Coherent);
-
-            if (isImage)
-            {
-                SetUsedTextureOrImage(_usedImages, cbufSlot, handle, type, format, true, isWrite, false, coherent);
-            }
-            else
-            {
-                bool intCoords = flags.HasFlag(TextureFlags.IntCoords) || inst == Instruction.TextureSize;
-                SetUsedTextureOrImage(_usedTextures, cbufSlot, handle, type, TextureFormat.Unknown, intCoords, false, accurateType, coherent);
-            }
-
-            GpuAccessor.RegisterTexture(handle, cbufSlot);
-        }
-
-        private void SetUsedTextureOrImage(
-            Dictionary<TextureInfo, TextureMeta> dict,
-            int cbufSlot,
-            int handle,
-            SamplerType type,
-            TextureFormat format,
-            bool intCoords,
-            bool write,
-            bool accurateType,
-            bool coherent)
-        {
-            var dimensions = type.GetDimensions();
-            var isIndexed = type.HasFlag(SamplerType.Indexed);
-
-            var usageFlags = TextureUsageFlags.None;
-
-            if (intCoords)
-            {
-                usageFlags |= TextureUsageFlags.NeedsScaleValue;
-
-                var canScale = Stage.SupportsRenderScale() && !isIndexed && !write && dimensions == 2;
-
-                if (!canScale)
-                {
-                    // Resolution scaling cannot be applied to this texture right now.
-                    // Flag so that we know to blacklist scaling on related textures when binding them.
-                    usageFlags |= TextureUsageFlags.ResScaleUnsupported;
-                }
-            }
-
-            if (write)
-            {
-                usageFlags |= TextureUsageFlags.ImageStore;
-            }
-
-            if (coherent)
-            {
-                usageFlags |= TextureUsageFlags.ImageCoherent;
-            }
-
-            int arraySize = isIndexed ? SamplerArraySize : 1;
-
-            for (int layer = 0; layer < arraySize; layer++)
-            {
-                var info = new TextureInfo(cbufSlot, handle + layer * 2, isIndexed, format);
-                var meta = new TextureMeta()
-                {
-                    AccurateType = accurateType,
-                    Type = type,
-                    UsageFlags = usageFlags,
-                };
-
-                if (dict.TryGetValue(info, out var existingMeta))
-                {
-                    dict[info] = MergeTextureMeta(meta, existingMeta);
-                }
-                else
-                {
-                    dict.Add(info, meta);
-                }
-            }
-        }
-
-        private static TextureMeta MergeTextureMeta(TextureMeta meta, TextureMeta existingMeta)
-        {
-            meta.UsageFlags |= existingMeta.UsageFlags;
-
-            // If the texture we have has inaccurate type information, then
-            // we prefer the most accurate one.
-            if (existingMeta.AccurateType)
-            {
-                meta.AccurateType = true;
-                meta.Type = existingMeta.Type;
-            }
-
-            return meta;
-        }
-
-        public TextureDescriptor[] GetTextureDescriptors()
-        {
-            return _cachedTextureDescriptors ??= GetTextureOrImageDescriptors(_usedTextures, GpuAccessor.QueryBindingTexture);
-        }
-
-        public TextureDescriptor[] GetImageDescriptors()
-        {
-            return _cachedImageDescriptors ??= GetTextureOrImageDescriptors(_usedImages, GpuAccessor.QueryBindingImage);
-        }
-
-        private static TextureDescriptor[] GetTextureOrImageDescriptors(Dictionary<TextureInfo, TextureMeta> dict, Func<int, bool, int> getBindingCallback)
-        {
-            var descriptors = new TextureDescriptor[dict.Count];
-
-            int i = 0;
-            foreach (var kv in dict.OrderBy(x => x.Key.Indexed).ThenBy(x => x.Key.Handle))
-            {
-                var info = kv.Key;
-                var meta = kv.Value;
-
-                bool isBuffer = (meta.Type & SamplerType.Mask) == SamplerType.TextureBuffer;
-                int binding = getBindingCallback(i, isBuffer);
-
-                descriptors[i] = new TextureDescriptor(binding, meta.Type, info.Format, info.CbufSlot, info.Handle);
-                descriptors[i].SetFlag(meta.UsageFlags);
-                i++;
-            }
-
-            return descriptors;
-        }
-
-        public TextureDescriptor FindTextureDescriptor(AstTextureOperation texOp)
-        {
-            TextureDescriptor[] descriptors = GetTextureDescriptors();
-
-            for (int i = 0; i < descriptors.Length; i++)
-            {
-                var descriptor = descriptors[i];
-
-                if (descriptor.CbufSlot == texOp.CbufSlot &&
-                    descriptor.HandleIndex == texOp.Handle &&
-                    descriptor.Format == texOp.Format)
-                {
-                    return descriptor;
-                }
-            }
-
-            return default;
-        }
-
-        private static int FindDescriptorIndex(TextureDescriptor[] array, TextureOperation texOp, bool ignoreType = false)
-        {
-            for (int i = 0; i < array.Length; i++)
-            {
-                var descriptor = array[i];
-
-                if ((descriptor.Type == texOp.Type || ignoreType) &&
-                    descriptor.CbufSlot == texOp.CbufSlot &&
-                    descriptor.HandleIndex == texOp.Handle &&
-                    descriptor.Format == texOp.Format)
-                {
-                    return i;
-                }
-            }
-
-            return -1;
-        }
-
-        public int FindTextureDescriptorIndex(TextureOperation texOp, bool ignoreType = false)
-        {
-            return FindDescriptorIndex(GetTextureDescriptors(), texOp, ignoreType);
-        }
-
-        public int FindImageDescriptorIndex(TextureOperation texOp)
-        {
-            return FindDescriptorIndex(GetImageDescriptors(), texOp);
-        }
-
         public ShaderProgramInfo CreateProgramInfo(ShaderIdentification identification = ShaderIdentification.None)
         {
             return new ShaderProgramInfo(
                 ResourceManager.GetConstantBufferDescriptors(),
                 ResourceManager.GetStorageBufferDescriptors(),
-                GetTextureDescriptors(),
-                GetImageDescriptors(),
+                ResourceManager.GetTextureDescriptors(),
+                ResourceManager.GetImageDescriptors(),
                 identification,
                 GpLayerInputAttribute,
                 Stage,