diff --git a/src/Ryujinx.Graphics.GAL/IPipeline.cs b/src/Ryujinx.Graphics.GAL/IPipeline.cs
index f5978cefa..57d65a3a5 100644
--- a/src/Ryujinx.Graphics.GAL/IPipeline.cs
+++ b/src/Ryujinx.Graphics.GAL/IPipeline.cs
@@ -42,6 +42,10 @@ namespace Ryujinx.Graphics.GAL
 
         void EndTransformFeedback();
 
+        void RegisterBindlessSampler(int samplerId, ISampler sampler);
+        void RegisterBindlessTexture(int textureId, ITexture texture, float textureScale);
+        void RegisterBindlessTextureAndSampler(int textureId, ITexture texture, float textureScale, int samplerId, ISampler sampler);
+
         void SetAlphaTest(bool enable, float reference, CompareOp op);
 
         void SetBlendState(AdvancedBlendDescriptor blend);
diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/CommandHelper.cs b/src/Ryujinx.Graphics.GAL/Multithreading/CommandHelper.cs
index 6cd6f1599..6e43126d9 100644
--- a/src/Ryujinx.Graphics.GAL/Multithreading/CommandHelper.cs
+++ b/src/Ryujinx.Graphics.GAL/Multithreading/CommandHelper.cs
@@ -100,6 +100,9 @@ namespace Ryujinx.Graphics.GAL.Multithreading
             Register<DrawTextureCommand>(CommandType.DrawTexture);
             Register<EndHostConditionalRenderingCommand>(CommandType.EndHostConditionalRendering);
             Register<EndTransformFeedbackCommand>(CommandType.EndTransformFeedback);
+            Register<RegisterBindlessSamplerCommand>(CommandType.RegisterBindlessSampler);
+            Register<RegisterBindlessTextureCommand>(CommandType.RegisterBindlessTexture);
+            Register<RegisterBindlessTextureAndSamplerCommand>(CommandType.RegisterBindlessTextureAndSampler);
             Register<SetAlphaTestCommand>(CommandType.SetAlphaTest);
             Register<SetBlendStateAdvancedCommand>(CommandType.SetBlendStateAdvanced);
             Register<SetBlendStateCommand>(CommandType.SetBlendState);
diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/CommandType.cs b/src/Ryujinx.Graphics.GAL/Multithreading/CommandType.cs
index c24a934aa..89f9b1911 100644
--- a/src/Ryujinx.Graphics.GAL/Multithreading/CommandType.cs
+++ b/src/Ryujinx.Graphics.GAL/Multithreading/CommandType.cs
@@ -62,6 +62,9 @@
         DrawTexture,
         EndHostConditionalRendering,
         EndTransformFeedback,
+        RegisterBindlessSampler,
+        RegisterBindlessTexture,
+        RegisterBindlessTextureAndSampler,
         SetAlphaTest,
         SetBlendStateAdvanced,
         SetBlendState,
diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/Commands/RegisterBindlessSamplerCommand.cs b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/RegisterBindlessSamplerCommand.cs
new file mode 100644
index 000000000..ec26ca38d
--- /dev/null
+++ b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/RegisterBindlessSamplerCommand.cs
@@ -0,0 +1,23 @@
+using Ryujinx.Graphics.GAL.Multithreading.Model;
+using Ryujinx.Graphics.GAL.Multithreading.Resources;
+
+namespace Ryujinx.Graphics.GAL.Multithreading.Commands
+{
+    struct RegisterBindlessSamplerCommand : IGALCommand, IGALCommand<RegisterBindlessSamplerCommand>
+    {
+        public CommandType CommandType => CommandType.RegisterBindlessSampler;
+        private int _samplerId;
+        private TableRef<ISampler> _sampler;
+
+        public void Set(int samplerId, TableRef<ISampler> sampler)
+        {
+            _samplerId = samplerId;
+            _sampler = sampler;
+        }
+
+        public static void Run(ref RegisterBindlessSamplerCommand command, ThreadedRenderer threaded, IRenderer renderer)
+        {
+            renderer.Pipeline.RegisterBindlessSampler(command._samplerId, command._sampler.GetAs<ThreadedSampler>(threaded)?.Base);
+        }
+    }
+}
diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/Commands/RegisterBindlessTextureAndSamplerCommand.cs b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/RegisterBindlessTextureAndSamplerCommand.cs
new file mode 100644
index 000000000..27463cce6
--- /dev/null
+++ b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/RegisterBindlessTextureAndSamplerCommand.cs
@@ -0,0 +1,34 @@
+using Ryujinx.Graphics.GAL.Multithreading.Model;
+using Ryujinx.Graphics.GAL.Multithreading.Resources;
+
+namespace Ryujinx.Graphics.GAL.Multithreading.Commands
+{
+    struct RegisterBindlessTextureAndSamplerCommand : IGALCommand, IGALCommand<RegisterBindlessTextureAndSamplerCommand>
+    {
+        public CommandType CommandType => CommandType.RegisterBindlessTextureAndSampler;
+        private int _textureId;
+        private int _samplerId;
+        private TableRef<ITexture> _texture;
+        private TableRef<ISampler> _sampler;
+        private float _textureScale;
+
+        public void Set(int textureId, TableRef<ITexture> texture, float textureScale, int samplerId, TableRef<ISampler> sampler)
+        {
+            _textureId = textureId;
+            _samplerId = samplerId;
+            _textureScale = textureScale;
+            _texture = texture;
+            _sampler = sampler;
+        }
+
+        public static void Run(ref RegisterBindlessTextureAndSamplerCommand command, ThreadedRenderer threaded, IRenderer renderer)
+        {
+            renderer.Pipeline.RegisterBindlessTextureAndSampler(
+                command._textureId,
+                command._texture.GetAs<ThreadedTexture>(threaded)?.Base,
+                command._textureScale,
+                command._samplerId,
+                command._sampler.GetAs<ThreadedSampler>(threaded)?.Base);
+        }
+    }
+}
diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/Commands/RegisterBindlessTextureCommand.cs b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/RegisterBindlessTextureCommand.cs
new file mode 100644
index 000000000..1e3389844
--- /dev/null
+++ b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/RegisterBindlessTextureCommand.cs
@@ -0,0 +1,25 @@
+using Ryujinx.Graphics.GAL.Multithreading.Model;
+using Ryujinx.Graphics.GAL.Multithreading.Resources;
+
+namespace Ryujinx.Graphics.GAL.Multithreading.Commands
+{
+    struct RegisterBindlessTextureCommand : IGALCommand, IGALCommand<RegisterBindlessTextureCommand>
+    {
+        public CommandType CommandType => CommandType.RegisterBindlessTexture;
+        private int _textureId;
+        private TableRef<ITexture> _texture;
+        private float _textureScale;
+
+        public void Set(int textureId, TableRef<ITexture> texture, float textureScale)
+        {
+            _textureId = textureId;
+            _texture = texture;
+            _textureScale = textureScale;
+        }
+
+        public static void Run(ref RegisterBindlessTextureCommand command, ThreadedRenderer threaded, IRenderer renderer)
+        {
+            renderer.Pipeline.RegisterBindlessTexture(command._textureId, command._texture.GetAs<ThreadedTexture>(threaded)?.Base, command._textureScale);
+        }
+    }
+}
diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedPipeline.cs b/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedPipeline.cs
index 69c67d642..6ea389b15 100644
--- a/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedPipeline.cs
+++ b/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedPipeline.cs
@@ -123,6 +123,24 @@ namespace Ryujinx.Graphics.GAL.Multithreading
             _renderer.QueueCommand();
         }
 
+        public void RegisterBindlessSampler(int samplerId, ISampler sampler)
+        {
+            _renderer.New<RegisterBindlessSamplerCommand>().Set(samplerId, Ref(sampler));
+            _renderer.QueueCommand();
+        }
+
+        public void RegisterBindlessTexture(int textureId, ITexture texture, float textureScale)
+        {
+            _renderer.New<RegisterBindlessTextureCommand>().Set(textureId, Ref(texture), textureScale);
+            _renderer.QueueCommand();
+        }
+
+        public void RegisterBindlessTextureAndSampler(int textureId, ITexture texture, float textureScale, int samplerId, ISampler sampler)
+        {
+            _renderer.New<RegisterBindlessTextureAndSamplerCommand>().Set(textureId, Ref(texture), textureScale, samplerId, Ref(sampler));
+            _renderer.QueueCommand();
+        }
+
         public void SetAlphaTest(bool enable, float reference, CompareOp op)
         {
             _renderer.New<SetAlphaTestCommand>().Set(enable, reference, op);
diff --git a/src/Ryujinx.Graphics.GAL/ShaderInfo.cs b/src/Ryujinx.Graphics.GAL/ShaderInfo.cs
index 2fd3227dc..6c24ec25f 100644
--- a/src/Ryujinx.Graphics.GAL/ShaderInfo.cs
+++ b/src/Ryujinx.Graphics.GAL/ShaderInfo.cs
@@ -3,21 +3,24 @@ namespace Ryujinx.Graphics.GAL
     public struct ShaderInfo
     {
         public int FragmentOutputMap { get; }
+        public bool HasBindless { get; }
         public ResourceLayout ResourceLayout { get; }
         public ProgramPipelineState? State { get; }
         public bool FromCache { get; set; }
 
-        public ShaderInfo(int fragmentOutputMap, ResourceLayout resourceLayout, ProgramPipelineState state, bool fromCache = false)
+        public ShaderInfo(int fragmentOutputMap, bool hasBindless, ResourceLayout resourceLayout, ProgramPipelineState state, bool fromCache = false)
         {
             FragmentOutputMap = fragmentOutputMap;
+            HasBindless = hasBindless;
             ResourceLayout = resourceLayout;
             State = state;
             FromCache = fromCache;
         }
 
-        public ShaderInfo(int fragmentOutputMap, ResourceLayout resourceLayout, bool fromCache = false)
+        public ShaderInfo(int fragmentOutputMap, bool hasBindless, ResourceLayout resourceLayout, bool fromCache = false)
         {
             FragmentOutputMap = fragmentOutputMap;
+            HasBindless = hasBindless;
             ResourceLayout = resourceLayout;
             State = null;
             FromCache = fromCache;
diff --git a/src/Ryujinx.Graphics.Gpu/Constants.cs b/src/Ryujinx.Graphics.Gpu/Constants.cs
index c553d988e..6da27e8b7 100644
--- a/src/Ryujinx.Graphics.Gpu/Constants.cs
+++ b/src/Ryujinx.Graphics.Gpu/Constants.cs
@@ -89,5 +89,30 @@ namespace Ryujinx.Graphics.Gpu
         /// Maximum size that an storage buffer is assumed to have when the correct size is unknown.
         /// </summary>
         public const ulong MaxUnknownStorageSize = 0x100000;
+
+        /// <summary>
+        /// Maximum width and height for 1D, 2D and cube textures, including array and multisample variants.
+        /// </summary>
+        public const int MaxTextureSize = 0x4000;
+
+        /// <summary>
+        /// Maximum width, height and depth for 3D textures.
+        /// </summary>
+        public const int Max3DTextureSize = 0x800;
+
+        /// <summary>
+        /// Maximum layers for array textures.
+        /// </summary>
+        public const int MaxArrayTextureLayers = 0x800;
+
+        /// <summary>
+        /// Maximum width (effectively the size in pixels) for buffer textures.
+        /// </summary>
+        public const int MaxBufferTextureSize = 0x8000000;
+
+        /// <summary>
+        /// Alignment in bytes for pitch linear textures.
+        /// </summary>
+        public const int LinearStrideAlignment = 0x20;
     }
 }
diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Compute/ComputeClass.cs b/src/Ryujinx.Graphics.Gpu/Engine/Compute/ComputeClass.cs
index 67743de37..b96d9ce91 100644
--- a/src/Ryujinx.Graphics.Gpu/Engine/Compute/ComputeClass.cs
+++ b/src/Ryujinx.Graphics.Gpu/Engine/Compute/ComputeClass.cs
@@ -195,7 +195,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Compute
 
             // Should never return false for mismatching spec state, since the shader was fetched above.
             _channel.TextureManager.CommitComputeBindings(cs.SpecializationState);
-
             _channel.BufferManager.CommitComputeBindings();
 
             _context.Renderer.Pipeline.DispatchCompute(qmd.CtaRasterWidth, qmd.CtaRasterHeight, qmd.CtaRasterDepth);
diff --git a/src/Ryujinx.Graphics.Gpu/Engine/ShaderTexture.cs b/src/Ryujinx.Graphics.Gpu/Engine/ShaderTexture.cs
index 40d9a97df..065067331 100644
--- a/src/Ryujinx.Graphics.Gpu/Engine/ShaderTexture.cs
+++ b/src/Ryujinx.Graphics.Gpu/Engine/ShaderTexture.cs
@@ -16,7 +16,7 @@ namespace Ryujinx.Graphics.Gpu.Engine
         /// <returns>Texture target value</returns>
         public static Target GetTarget(SamplerType type)
         {
-            type &= ~(SamplerType.Indexed | SamplerType.Shadow);
+            type &= ~SamplerType.Shadow;
 
             switch (type)
             {
diff --git a/src/Ryujinx.Graphics.Gpu/Image/BitMap.cs b/src/Ryujinx.Graphics.Gpu/Image/BitMap.cs
new file mode 100644
index 000000000..18b9c4e17
--- /dev/null
+++ b/src/Ryujinx.Graphics.Gpu/Image/BitMap.cs
@@ -0,0 +1,197 @@
+using System.Numerics;
+
+namespace Ryujinx.Graphics.Gpu.Image
+{
+    /// <summary>
+    /// Represents a list of bits.
+    /// </summary>
+    class BitMap
+    {
+        private const int IntSize = 64;
+
+        private const int IntShift = 6;
+        private const int IntMask = IntSize - 1;
+
+        private readonly ulong[] _masks;
+
+        /// <summary>
+        /// Creates a new instance of the bitmap.
+        /// </summary>
+        /// <param name="count">Size (in bits) that the bitmap can hold</param>
+        public BitMap(int count)
+        {
+            _masks = new ulong[(count + IntMask) / IntSize];
+        }
+
+        /// <summary>
+        /// Sets a bit to 1.
+        /// </summary>
+        /// <param name="bit">Index of the bit</param>
+        /// <returns>True if the bit value was modified by this operation, false otherwise</returns>
+        public bool Set(int bit)
+        {
+            int wordIndex = bit / IntSize;
+            int wordBit   = bit & IntMask;
+
+            ulong wordMask = 1UL << wordBit;
+
+            if ((_masks[wordIndex] & wordMask) != 0)
+            {
+                return false;
+            }
+
+            _masks[wordIndex] |= wordMask;
+
+            return true;
+        }
+
+        /// <summary>
+        /// Sets a range of bits to 1.
+        /// </summary>
+        /// <param name="start">Inclusive index of the first bit to set</param>
+        /// <param name="end">Inclusive index of the last bit to set</param>
+        public void SetRange(int start, int end)
+        {
+            if (start == end)
+            {
+                Set(start);
+                return;
+            }
+
+            int startIndex = start >> IntShift;
+            int startBit = start & IntMask;
+            ulong startMask = ulong.MaxValue << startBit;
+
+            int endIndex = end >> IntShift;
+            int endBit = end & IntMask;
+            ulong endMask = ulong.MaxValue >> (IntMask - endBit);
+
+            if (startIndex == endIndex)
+            {
+                _masks[startIndex] |= startMask & endMask;
+            }
+            else
+            {
+                _masks[startIndex] |= startMask;
+
+                for (int i = startIndex + 1; i < endIndex; i++)
+                {
+                    _masks[i] = ulong.MaxValue;
+                }
+
+                _masks[endIndex] |= endMask;
+            }
+        }
+
+        /// <summary>
+        /// Sets a bit to 0.
+        /// </summary>
+        /// <param name="bit">Index of the bit</param>
+        public void Clear(int bit)
+        {
+            int wordIndex = bit / IntSize;
+            int wordBit   = bit & IntMask;
+
+            ulong wordMask = 1UL << wordBit;
+
+            _masks[wordIndex] &= ~wordMask;
+        }
+
+        /// <summary>
+        /// Finds the first bit with a value of 0.
+        /// </summary>
+        /// <returns>Index of the bit with value 0, or -1 if none found</returns>
+        public int FindFirstUnset()
+        {
+            int index = 0;
+
+            while (index < _masks.Length && _masks[index] == ulong.MaxValue)
+            {
+                index++;
+            }
+
+            if (index == _masks.Length)
+            {
+                return -1;
+            }
+
+            int bit = index * IntSize;
+
+            bit += BitOperations.TrailingZeroCount(~_masks[index]);
+
+            return bit;
+        }
+
+        private int _iterIndex;
+        private ulong _iterMask;
+
+        /// <summary>
+        /// Starts iterating from bit 0.
+        /// </summary>
+        public void BeginIterating()
+        {
+            _iterIndex = 0;
+            _iterMask = _masks.Length != 0 ? _masks[0] : 0;
+        }
+
+        /// <summary>
+        /// Gets the next bit set to 1.
+        /// </summary>
+        /// <returns>Index of the bit, or -1 if none found</returns>
+        public int GetNext()
+        {
+            if (_iterIndex >= _masks.Length)
+            {
+                return -1;
+            }
+
+            while (_iterMask == 0 && _iterIndex + 1 < _masks.Length)
+            {
+                _iterMask = _masks[++_iterIndex];
+            }
+
+            if (_iterMask == 0)
+            {
+                return -1;
+            }
+
+            int bit = BitOperations.TrailingZeroCount(_iterMask);
+
+            _iterMask &= ~(1UL << bit);
+
+            return _iterIndex * IntSize + bit;
+        }
+
+        /// <summary>
+        /// Gets the next bit set to 1, while also setting it to 0.
+        /// </summary>
+        /// <returns>Index of the bit, or -1 if none found</returns>
+        public int GetNextAndClear()
+        {
+            if (_iterIndex >= _masks.Length)
+            {
+                return -1;
+            }
+
+            ulong mask = _masks[_iterIndex];
+
+            while (mask == 0 && _iterIndex + 1 < _masks.Length)
+            {
+                mask = _masks[++_iterIndex];
+            }
+
+            if (mask == 0)
+            {
+                return -1;
+            }
+
+            int bit = BitOperations.TrailingZeroCount(mask);
+
+            mask &= ~(1UL << bit);
+
+            _masks[_iterIndex] = mask;
+
+            return _iterIndex * IntSize + bit;
+        }
+    }
+}
diff --git a/src/Ryujinx.Graphics.Gpu/Image/Pool.cs b/src/Ryujinx.Graphics.Gpu/Image/Pool.cs
index 0c3a219de..d48a901e6 100644
--- a/src/Ryujinx.Graphics.Gpu/Image/Pool.cs
+++ b/src/Ryujinx.Graphics.Gpu/Image/Pool.cs
@@ -22,6 +22,11 @@ namespace Ryujinx.Graphics.Gpu.Image
         protected T1[] Items;
         protected T2[] DescriptorCache;
 
+        protected readonly BitMap ModifiedEntries;
+
+        private int _minimumAccessedId;
+        private int _maximumAccessedId;
+
         /// <summary>
         /// The maximum ID value of resources on the pool (inclusive).
         /// </summary>
@@ -61,6 +66,11 @@ namespace Ryujinx.Graphics.Gpu.Image
 
             int count = maximumId + 1;
 
+            ModifiedEntries = new BitMap(count);
+
+            _minimumAccessedId = int.MaxValue;
+            _maximumAccessedId = 0;
+
             ulong size = (ulong)(uint)count * DescriptorSize;
 
             Items = new T1[count];
@@ -197,6 +207,41 @@ namespace Ryujinx.Graphics.Gpu.Image
             return false;
         }
 
+        /// <summary>
+        /// Updates the set of entries that have been modified.
+        /// </summary>
+        /// <param name="address">Start address of the region of the pool that has been modfied</param>
+        /// <param name="endAddress">End address of the region of the pool that has been modified, exclusive</param>
+        protected void UpdateModifiedEntries(ulong address, ulong endAddress)
+        {
+            int startId = (int)((address - Address) / DescriptorSize);
+            int endId = (int)((endAddress - Address + (DescriptorSize - 1)) / DescriptorSize) - 1;
+
+            if (endId < startId)
+            {
+                return;
+            }
+
+            ModifiedEntries.SetRange(startId, endId);
+
+            _minimumAccessedId = Math.Min(_minimumAccessedId, startId);
+            _maximumAccessedId = Math.Max(_maximumAccessedId, endId);
+        }
+
+        /// <summary>
+        /// Forces all entries as modified, to be updated if any shader uses bindless textures.
+        /// </summary>
+        public void ForceModifiedEntries()
+        {
+            for (int id = _minimumAccessedId; id <= _maximumAccessedId; id++)
+            {
+                if (Items[id] != null)
+                {
+                    ModifiedEntries.Set(id);
+                }
+            }
+        }
+
         protected abstract void InvalidateRangeImpl(ulong address, ulong size);
 
         protected abstract void Delete(T1 item);
diff --git a/src/Ryujinx.Graphics.Gpu/Image/SamplerDescriptor.cs b/src/Ryujinx.Graphics.Gpu/Image/SamplerDescriptor.cs
index e04c31dfa..660676350 100644
--- a/src/Ryujinx.Graphics.Gpu/Image/SamplerDescriptor.cs
+++ b/src/Ryujinx.Graphics.Gpu/Image/SamplerDescriptor.cs
@@ -113,6 +113,24 @@ namespace Ryujinx.Graphics.Gpu.Image
             return (CompareOp)(((Word0 >> 10) & 7) + 1);
         }
 
+        /// <summary>
+        /// Unpacks the font filter width.
+        /// </summary>
+        /// <returns>Font filter width</returns>
+        public int UnpackFontFilterWidth()
+        {
+            return (int)(Word0 >> 14) & 7;
+        }
+
+        /// <summary>
+        /// Unpacks the font filter height.
+        /// </summary>
+        /// <returns>Font filter height</returns>
+        public int UnpackFontFilterHeight()
+        {
+            return (int)(Word0 >> 17) & 7;
+        }
+
         /// <summary>
         /// Unpacks and converts the maximum anisotropy value used for texture anisotropic filtering.
         /// </summary>
diff --git a/src/Ryujinx.Graphics.Gpu/Image/SamplerPool.cs b/src/Ryujinx.Graphics.Gpu/Image/SamplerPool.cs
index 3efcad760..42351528d 100644
--- a/src/Ryujinx.Graphics.Gpu/Image/SamplerPool.cs
+++ b/src/Ryujinx.Graphics.Gpu/Image/SamplerPool.cs
@@ -1,4 +1,5 @@
 using Ryujinx.Graphics.Gpu.Memory;
+using System;
 using System.Collections.Generic;
 
 namespace Ryujinx.Graphics.Gpu.Image
@@ -117,6 +118,53 @@ namespace Ryujinx.Graphics.Gpu.Image
             return ModifiedSequenceNumber;
         }
 
+        /// <summary>
+        /// Loads all the samplers currently registered by the guest application on the pool.
+        /// This is required for bindless access, as it's not possible to predict which sampler will be used.
+        /// </summary>
+        public void LoadAll()
+        {
+            if (SequenceNumber != Context.SequenceNumber)
+            {
+                SequenceNumber = Context.SequenceNumber;
+
+                SynchronizeMemory();
+            }
+
+            ModifiedEntries.BeginIterating();
+
+            int id;
+
+            while ((id = ModifiedEntries.GetNextAndClear()) >= 0)
+            {
+                Sampler sampler = Items[id] ?? GetValidated(id);
+
+                if (sampler != null)
+                {
+                    Context.Renderer.Pipeline.RegisterBindlessSampler(id, sampler.GetHostSampler(null));
+                }
+            }
+        }
+
+        /// <summary>
+        /// Gets the sampler at the given <paramref name="id"/> from the cache,
+        /// or creates a new one if not found.
+        /// This will return null if the sampler entry is considered invalid.
+        /// </summary>
+        /// <param name="id">Index of the sampler on the pool</param>
+        /// <returns>Sampler for the given pool index</returns>
+        private Sampler GetValidated(int id)
+        {
+            SamplerDescriptor descriptor = GetDescriptor(id);
+
+            if (descriptor.UnpackFontFilterWidth() != 1 || descriptor.UnpackFontFilterHeight() != 1 || (descriptor.Word0 >> 23) != 0)
+            {
+                return null;
+            }
+
+            return Get(id);
+        }
+
         /// <summary>
         /// Implementation of the sampler pool range invalidation.
         /// </summary>
@@ -126,6 +174,8 @@ namespace Ryujinx.Graphics.Gpu.Image
         {
             ulong endAddress = address + size;
 
+            UpdateModifiedEntries(address, endAddress);
+
             for (; address < endAddress; address += DescriptorSize)
             {
                 int id = (int)((address - Address) / DescriptorSize);
diff --git a/src/Ryujinx.Graphics.Gpu/Image/Texture.cs b/src/Ryujinx.Graphics.Gpu/Image/Texture.cs
index f1615b388..5742bae9a 100644
--- a/src/Ryujinx.Graphics.Gpu/Image/Texture.cs
+++ b/src/Ryujinx.Graphics.Gpu/Image/Texture.cs
@@ -1462,6 +1462,19 @@ namespace Ryujinx.Graphics.Gpu.Image
             DisposeTextures();
 
             HostTexture = hostTexture;
+
+            ForceTexturePoolUpdate();
+        }
+
+        /// <summary>
+        /// Forces the entries on all texture pool where this texture is present to be updated.
+        /// </summary>
+        private void ForceTexturePoolUpdate()
+        {
+            foreach (TexturePoolOwner poolOwner in _poolOwners)
+            {
+                poolOwner.Pool.ForceModifiedEntry(poolOwner.ID);
+            }
         }
 
         /// <summary>
diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureBindingsManager.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureBindingsManager.cs
index 8eca18b48..ea2bd00ba 100644
--- a/src/Ryujinx.Graphics.Gpu/Image/TextureBindingsManager.cs
+++ b/src/Ryujinx.Graphics.Gpu/Image/TextureBindingsManager.cs
@@ -5,6 +5,7 @@ using Ryujinx.Graphics.Gpu.Memory;
 using Ryujinx.Graphics.Gpu.Shader;
 using Ryujinx.Graphics.Shader;
 using System;
+using System.Collections.Generic;
 using System.Runtime.CompilerServices;
 using System.Runtime.InteropServices;
 
@@ -59,6 +60,8 @@ namespace Ryujinx.Graphics.Gpu.Image
         private int _texturePoolSequence;
         private int _samplerPoolSequence;
 
+        private BindlessTextureFlags[] _bindlessTextureFlags;
+
         private int _textureBufferIndex;
 
         private int _lastFragmentTotal;
@@ -93,6 +96,8 @@ namespace Ryujinx.Graphics.Gpu.Image
             _textureState = new TextureState[InitialTextureStateSize];
             _imageState = new TextureState[InitialImageStateSize];
 
+            _bindlessTextureFlags = new BindlessTextureFlags[stages];
+
             for (int stage = 0; stage < stages; stage++)
             {
                 _textureBindings[stage] = new TextureBindingInfo[InitialTextureStateSize];
@@ -109,6 +114,8 @@ namespace Ryujinx.Graphics.Gpu.Image
             _textureBindings = bindings.TextureBindings;
             _imageBindings = bindings.ImageBindings;
 
+            _bindlessTextureFlags = bindings.BindlessTextureFlags;
+
             SetMaxBindings(bindings.MaxTextureBinding, bindings.MaxImageBinding);
         }
 
@@ -305,6 +312,23 @@ namespace Ryujinx.Graphics.Gpu.Image
             // If it wasn't, then it's possible to avoid looking up textures again when the handle remains the same.
             if (_cachedTexturePool != texturePool || _cachedSamplerPool != samplerPool)
             {
+                bool anyFullBindless = false;
+
+                for (int index = 0; index < (_isCompute ? 1 : _bindlessTextureFlags.Length); index++)
+                {
+                    if (_bindlessTextureFlags[index].HasFlag(BindlessTextureFlags.BindlessFull))
+                    {
+                        anyFullBindless = true;
+                        break;
+                    }
+                }
+
+                if (anyFullBindless)
+                {
+                    texturePool?.ForceModifiedEntries();
+                    samplerPool?.ForceModifiedEntries();
+                }
+
                 Rebind();
 
                 _cachedTexturePool = texturePool;
@@ -341,6 +365,15 @@ namespace Ryujinx.Graphics.Gpu.Image
             {
                 specStateMatches &= CommitTextureBindings(texturePool, samplerPool, ShaderStage.Compute, 0, poolModified, specState);
                 specStateMatches &= CommitImageBindings(texturePool, ShaderStage.Compute, 0, poolModified, specState);
+
+                if (_bindlessTextureFlags[0].HasFlag(BindlessTextureFlags.BindlessNvn))
+                {
+                    CommitBindlessResources(texturePool, ShaderStage.Compute, 0);
+                }
+                else if (_bindlessTextureFlags[0].HasFlag(BindlessTextureFlags.BindlessFull))
+                {
+                    texturePool.LoadAll(_context.Renderer, _samplerPool);
+                }
             }
             else
             {
@@ -350,6 +383,15 @@ namespace Ryujinx.Graphics.Gpu.Image
 
                     specStateMatches &= CommitTextureBindings(texturePool, samplerPool, stage, stageIndex, poolModified, specState);
                     specStateMatches &= CommitImageBindings(texturePool, stage, stageIndex, poolModified, specState);
+
+                    if (_bindlessTextureFlags[stageIndex].HasFlag(BindlessTextureFlags.BindlessNvn))
+                    {
+                        CommitBindlessResources(texturePool, stage, stageIndex);
+                    }
+                    else if (_bindlessTextureFlags[stageIndex].HasFlag(BindlessTextureFlags.BindlessFull))
+                    {
+                        texturePool.LoadAll(_context.Renderer, _samplerPool);
+                    }
                 }
             }
 
@@ -460,8 +502,12 @@ namespace Ryujinx.Graphics.Gpu.Image
             ReadOnlySpan<int> cachedTextureBuffer = Span<int>.Empty;
             ReadOnlySpan<int> cachedSamplerBuffer = Span<int>.Empty;
 
+            int maxTexturesPerStage = TextureHandle.GetMaxTexturesPerStage(_context.Capabilities.Api);
+
             for (int index = 0; index < textureCount; index++)
             {
+                bool asBindless = index >= maxTexturesPerStage;
+
                 TextureBindingInfo bindingInfo = _textureBindings[stageIndex][index];
                 TextureUsageFlags usageFlags = bindingInfo.Flags;
 
@@ -524,7 +570,17 @@ namespace Ryujinx.Graphics.Gpu.Image
                     // Ensure that the buffer texture is using the correct buffer as storage.
                     // Buffers are frequently re-created to accommodate larger data, so we need to re-bind
                     // to ensure we're not using a old buffer that was already deleted.
-                    _channel.BufferManager.SetBufferTextureStorage(stage, hostTexture, texture.Range.GetSubRange(0).Address, texture.Size, bindingInfo, bindingInfo.Format, false);
+                    ulong address = texture.Range.GetSubRange(0).Address;
+                    ulong size = texture.Size;
+
+                    if (asBindless)
+                    {
+                        _channel.BufferManager.SetBufferTextureStorage(hostTexture, address, size, bindingInfo, bindingInfo.Format, false, textureId);
+                    }
+                    else
+                    {
+                        _channel.BufferManager.SetBufferTextureStorage(stage, hostTexture, address, size, bindingInfo, bindingInfo.Format, false);
+                    }
 
                     // Cache is not used for buffer texture, it must always rebind.
                     state.CachedTexture = null;
@@ -545,7 +601,14 @@ namespace Ryujinx.Graphics.Gpu.Image
                         state.Texture = hostTexture;
                         state.Sampler = hostSampler;
 
-                        _context.Renderer.Pipeline.SetTextureAndSampler(stage, bindingInfo.Binding, hostTexture, hostSampler);
+                        if (asBindless)
+                        {
+                            _context.Renderer.Pipeline.RegisterBindlessTextureAndSampler(textureId, hostTexture, texture?.ScaleFactor ?? 1f, samplerId, hostSampler);
+                        }
+                        else
+                        {
+                            _context.Renderer.Pipeline.SetTextureAndSampler(stage, bindingInfo.Binding, hostTexture, hostSampler);
+                        }
                     }
 
                     state.CachedTexture = texture;
@@ -703,6 +766,93 @@ namespace Ryujinx.Graphics.Gpu.Image
             return specStateMatches;
         }
 
+        /// <summary>
+        /// Ensures that the texture bindings are visible to the host GPU.
+        /// Note: this actually performs the binding using the host graphics API.
+        /// </summary>
+        /// <param name="pool">The current texture pool</param>
+        /// <param name="stage">The shader stage using the textures to be bound</param>
+        /// <param name="stageIndex">The stage number of the specified shader stage</param>
+        private void CommitBindlessResources(TexturePool pool, ShaderStage stage, int stageIndex)
+        {
+            var samplerPool = _samplerPool;
+
+            if (pool == null)
+            {
+                Logger.Error?.Print(LogClass.Gpu, $"Shader stage \"{stage}\" uses bindless textures, but texture pool was not set.");
+                return;
+            }
+
+            for (int index = 0; index < 32; index++)
+            {
+                int wordOffset = 8 + index * 2;
+
+                int packedId = ReadConstantBuffer<int>(stageIndex, _textureBufferIndex, wordOffset);
+
+                int textureId = TextureHandle.UnpackTextureId(packedId);
+                int samplerId;
+
+                if (_samplerIndex == SamplerIndex.ViaHeaderIndex)
+                {
+                    samplerId = textureId;
+                }
+                else
+                {
+                    samplerId = TextureHandle.UnpackSamplerId(packedId);
+                }
+
+                Texture texture = pool.Get(textureId);
+
+                if (texture == null)
+                {
+                    continue;
+                }
+
+                if (texture.Target == Target.TextureBuffer)
+                {
+                    // Ensure that the buffer texture is using the correct buffer as storage.
+                    // Buffers are frequently re-created to accomodate larger data, so we need to re-bind
+                    // to ensure we're not using a old buffer that was already deleted.
+                    TextureBindingInfo bindingInfo = new TextureBindingInfo(texture.Target, texture.Format, 0, 0, 0, TextureUsageFlags.None);
+                    ulong address = texture.Range.GetSubRange(0).Address;
+                    ulong size = texture.Size;
+                    _channel.BufferManager.SetBufferTextureStorage(texture.HostTexture, address, size, bindingInfo, texture.Format, false, textureId);
+                }
+                else
+                {
+                    Sampler sampler = samplerPool?.Get(samplerId);
+
+                    if (sampler == null)
+                    {
+                        continue;
+                    }
+
+                    _context.Renderer.Pipeline.RegisterBindlessTextureAndSampler(
+                        textureId,
+                        texture.HostTexture,
+                        texture.ScaleFactor,
+                        samplerId,
+                        sampler.GetHostSampler(texture));
+                }
+            }
+        }
+
+        /// <summary>
+        /// Reads a value from a constant buffer.
+        /// </summary>
+        /// <param name="stageIndex">Index of the shader stage where the constant buffer belongs</param>
+        /// <param name="bufferIndex">Index of the constant buffer to read from</param>
+        /// <param name="elementIndex">Index of the element on the constant buffer</param>
+        /// <returns>The value at the specified buffer and offset</returns>
+        private unsafe T ReadConstantBuffer<T>(int stageIndex, int bufferIndex, int elementIndex) where T : unmanaged
+        {
+            ulong baseAddress = _isCompute
+                ? _channel.BufferManager.GetComputeUniformBufferAddress(bufferIndex)
+                : _channel.BufferManager.GetGraphicsUniformBufferAddress(stageIndex, bufferIndex);
+
+            return _channel.MemoryManager.Physical.Read<T>(baseAddress + (ulong)elementIndex * (ulong)sizeof(T));
+        }
+
         /// <summary>
         /// Gets the texture descriptor for a given texture handle.
         /// </summary>
@@ -749,14 +899,13 @@ namespace Ryujinx.Graphics.Gpu.Image
         }
 
         /// <summary>
-        /// Reads a packed texture and sampler ID (basically, the real texture handle)
-        /// from the texture constant buffer.
+        /// Reads a combined texture and sampler handle from the texture constant buffer.
         /// </summary>
         /// <param name="stageIndex">The number of the shader stage where the texture is bound</param>
-        /// <param name="wordOffset">A word offset of the handle on the buffer (the "fake" shader handle)</param>
+        /// <param name="wordOffset">The word offset of the handle on the buffer</param>
         /// <param name="textureBufferIndex">Index of the constant buffer holding the texture handles</param>
         /// <param name="samplerBufferIndex">Index of the constant buffer holding the sampler handles</param>
-        /// <returns>The packed texture and sampler ID (the real texture handle)</returns>
+        /// <returns>The combined texture and sampler handle</returns>
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
         private int ReadPackedId(int stageIndex, int wordOffset, int textureBufferIndex, int samplerBufferIndex)
         {
diff --git a/src/Ryujinx.Graphics.Gpu/Image/TexturePool.cs b/src/Ryujinx.Graphics.Gpu/Image/TexturePool.cs
index 0fdb6cd64..cbb163b5b 100644
--- a/src/Ryujinx.Graphics.Gpu/Image/TexturePool.cs
+++ b/src/Ryujinx.Graphics.Gpu/Image/TexturePool.cs
@@ -2,6 +2,7 @@ using Ryujinx.Common.Logging;
 using Ryujinx.Graphics.GAL;
 using Ryujinx.Graphics.Gpu.Memory;
 using Ryujinx.Graphics.Texture;
+using Ryujinx.Memory;
 using Ryujinx.Memory.Range;
 using System;
 using System.Collections.Concurrent;
@@ -212,6 +213,94 @@ namespace Ryujinx.Graphics.Gpu.Image
             return ModifiedSequenceNumber;
         }
 
+        /// <summary>
+        /// Loads all the textures currently registered by the guest application on the pool.
+        /// This is required for bindless access, as it's not possible to predict which textures will be used.
+        /// </summary>
+        /// <param name="renderer">Renderer of the current GPU context</param>
+        /// <param name="activeSamplerPool">The currently active sampler pool</param>
+        public void LoadAll(IRenderer renderer, SamplerPool activeSamplerPool)
+        {
+            activeSamplerPool?.LoadAll();
+
+            if (SequenceNumber != Context.SequenceNumber)
+            {
+                SequenceNumber = Context.SequenceNumber;
+
+                SynchronizeMemory();
+            }
+
+            ModifiedEntries.BeginIterating();
+
+            int id;
+
+            while ((id = ModifiedEntries.GetNextAndClear()) >= 0)
+            {
+                Texture texture = Items[id] ?? GetValidated(id);
+
+                if (texture != null)
+                {
+                    if (texture.Target == Target.TextureBuffer)
+                    {
+                        _channel.BufferManager.SetBufferTextureStorage(
+                            texture.HostTexture,
+                            texture.Range.GetSubRange(0).Address,
+                            texture.Size,
+                            default,
+                            0,
+                            false,
+                            id);
+                    }
+                    else
+                    {
+                        renderer.Pipeline.RegisterBindlessTexture(id, texture.HostTexture, texture.ScaleFactor);
+                    }
+                }
+            }
+        }
+
+        /// <summary>
+        /// Gets the texture at the given <paramref name="id"/> from the cache,
+        /// or creates a new one if not found.
+        /// This will return null if the texture entry is considered invalid.
+        /// </summary>
+        /// <param name="id">Index of the texture on the pool</param>
+        /// <returns>Texture for the given pool index</returns>
+        private Texture GetValidated(int id)
+        {
+            TextureDescriptor descriptor = GetDescriptor(id);
+
+            if (!FormatTable.TryGetTextureFormat(descriptor.UnpackFormat(), descriptor.UnpackSrgb(), out _))
+            {
+                return null;
+            }
+
+            TextureInfo info = GetInfo(descriptor, out int layerSize);
+            TextureValidationResult validationResult = TextureValidation.Validate(ref info);
+
+            if (validationResult != TextureValidationResult.Valid)
+            {
+                return null;
+            }
+
+            // TODO: Eventually get rid of that...
+            // For now it avoids creating textures for garbage entries in some cases, but it is not
+            // correct as a width or height of 8192 is valid (although extremely unlikely).
+            if (info.Width > 8192 || info.Height > 8192 || info.DepthOrLayers > 8192)
+            {
+                return null;
+            }
+
+            try
+            {
+                return Get(id);
+            }
+            catch (InvalidMemoryRegionException)
+            {
+                return null;
+            }
+        }
+
         /// <summary>
         /// Forcibly remove a texture from this pool's items.
         /// If deferred, the dereference will be queued to occur on the render thread.
@@ -342,6 +431,8 @@ namespace Ryujinx.Graphics.Gpu.Image
 
             ulong endAddress = address + size;
 
+            UpdateModifiedEntries(address, endAddress);
+
             for (; address < endAddress; address += DescriptorSize)
             {
                 int id = (int)((address - Address) / DescriptorSize);
@@ -373,6 +464,15 @@ namespace Ryujinx.Graphics.Gpu.Image
             }
         }
 
+        /// <summary>
+        /// Forces a entry as modified, to be updated if any shader uses bindless textures.
+        /// </summary>
+        /// <param name="id">ID of the entry to be updated</param>
+        public void ForceModifiedEntry(int id)
+        {
+            ModifiedEntries.Set(id);
+        }
+
         /// <summary>
         /// Gets texture information from a texture descriptor.
         /// </summary>
diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureValidation.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureValidation.cs
new file mode 100644
index 000000000..66e50e405
--- /dev/null
+++ b/src/Ryujinx.Graphics.Gpu/Image/TextureValidation.cs
@@ -0,0 +1,107 @@
+using Ryujinx.Graphics.GAL;
+
+namespace Ryujinx.Graphics.Gpu.Image
+{
+    /// <summary>
+    /// Texture validation result.
+    /// </summary>
+    enum TextureValidationResult
+    {
+        Valid,
+        InvalidSize,
+        InvalidTarget,
+        InvalidFormat
+    }
+
+    /// <summary>
+    /// Texture validation utilities.
+    /// </summary>
+    static class TextureValidation
+    {
+        /// <summary>
+        /// Checks if the texture parameters are valid.
+        /// </summary>
+        /// <param name="info">Texture parameters</param>
+        /// <returns>Validation result</returns>
+        public static TextureValidationResult Validate(ref TextureInfo info)
+        {
+            bool validSize;
+
+            switch (info.Target)
+            {
+                case Target.Texture1D:
+                    validSize = (uint)info.Width <= Constants.MaxTextureSize;
+                    break;
+                case Target.Texture2D:
+                    validSize = (uint)info.Width <= Constants.MaxTextureSize &&
+                                (uint)info.Height <= Constants.MaxTextureSize;
+                    break;
+                case Target.Texture3D:
+                    validSize = (uint)info.Width <= Constants.Max3DTextureSize &&
+                                (uint)info.Height <= Constants.Max3DTextureSize &&
+                                (uint)info.DepthOrLayers <= Constants.Max3DTextureSize;
+                    break;
+                case Target.Texture1DArray:
+                    validSize = (uint)info.Width <= Constants.MaxTextureSize &&
+                                (uint)info.DepthOrLayers <= Constants.MaxArrayTextureLayers;
+                    break;
+                case Target.Texture2DArray:
+                    validSize = (uint)info.Width <= Constants.MaxTextureSize &&
+                                (uint)info.Height <= Constants.MaxTextureSize &&
+                                (uint)info.DepthOrLayers <= Constants.MaxArrayTextureLayers;
+                    break;
+                case Target.Texture2DMultisample:
+                    validSize = (uint)info.Width <= Constants.MaxTextureSize &&
+                                (uint)info.Height <= Constants.MaxTextureSize;
+                    break;
+                case Target.Texture2DMultisampleArray:
+                    validSize = (uint)info.Width <= Constants.MaxTextureSize &&
+                                (uint)info.Height <= Constants.MaxTextureSize &&
+                                (uint)info.DepthOrLayers <= Constants.MaxArrayTextureLayers;
+                    break;
+                case Target.Cubemap:
+                    validSize = (uint)info.Width <= Constants.MaxTextureSize &&
+                                (uint)info.Height <= Constants.MaxTextureSize && info.Width == info.Height;
+                    break;
+                case Target.CubemapArray:
+                    validSize = (uint)info.Width <= Constants.MaxTextureSize &&
+                                (uint)info.Height <= Constants.MaxTextureSize &&
+                                (uint)info.DepthOrLayers <= Constants.MaxArrayTextureLayers && info.Width == info.Height;
+                    break;
+                case Target.TextureBuffer:
+                    validSize = (uint)info.Width <= Constants.MaxBufferTextureSize;
+                    break;
+                default:
+                    return TextureValidationResult.InvalidTarget;
+            }
+
+            if (!validSize)
+            {
+                return TextureValidationResult.InvalidSize;
+            }
+
+            if (info.IsLinear && (uint)info.Width > (uint)info.Stride)
+            {
+                return TextureValidationResult.InvalidSize;
+            }
+
+            return TextureValidationResult.Valid;
+        }
+
+        /// <summary>
+        /// Checks if a sampler can be used in combination with a given texture.
+        /// </summary>
+        /// <param name="info">Texture parameters</param>
+        /// <param name="sampler">Sampler parameters</param>
+        /// <returns>True if they can be used together, false otherwise</returns>
+        public static bool IsSamplerCompatible(TextureInfo info, SamplerDescriptor sampler)
+        {
+            if (info.FormatInfo.Format.IsDepthOrStencil() != (sampler.UnpackCompareMode() == CompareMode.CompareRToTexture))
+            {
+                return false;
+            }
+
+            return true;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Ryujinx.Graphics.Gpu/Memory/BufferManager.cs b/src/Ryujinx.Graphics.Gpu/Memory/BufferManager.cs
index 8e9b4b858..78519cabb 100644
--- a/src/Ryujinx.Graphics.Gpu/Memory/BufferManager.cs
+++ b/src/Ryujinx.Graphics.Gpu/Memory/BufferManager.cs
@@ -482,7 +482,11 @@ namespace Ryujinx.Graphics.Gpu.Memory
 
                     // The texture must be rebound to use the new storage if it was updated.
 
-                    if (binding.IsImage)
+                    if (binding.AsBindless)
+                    {
+                        _context.Renderer.Pipeline.RegisterBindlessTexture(binding.TextureId, binding.Texture, 1f);
+                    }
+                    else if (binding.IsImage)
                     {
                         _context.Renderer.Pipeline.SetImage(binding.BindingInfo.Binding, binding.Texture, binding.Format);
                     }
@@ -812,6 +816,30 @@ namespace Ryujinx.Graphics.Gpu.Memory
             _bufferTextures.Add(new BufferTextureBinding(stage, texture, address, size, bindingInfo, format, isImage));
         }
 
+        /// <summary>
+        /// Sets the buffer storage of a bindless buffer texture. This will be bound when the buffer manager commits bindings.
+        /// </summary>
+        /// <param name="texture">Buffer texture</param>
+        /// <param name="address">Address of the buffer in memory</param>
+        /// <param name="size">Size of the buffer in bytes</param>
+        /// <param name="bindingInfo">Binding info for the buffer texture</param>
+        /// <param name="format">Format of the buffer texture</param>
+        /// <param name="isImage">Whether the binding is for an image or a sampler</param>
+        /// <param name="textureid">ID of the texture on the pool/param>
+        public void SetBufferTextureStorage(
+            ITexture texture,
+            ulong address,
+            ulong size,
+            TextureBindingInfo bindingInfo,
+            Format format,
+            bool isImage,
+            int textureId)
+        {
+            _channel.MemoryManager.Physical.BufferCache.CreateBuffer(address, size);
+
+            _bufferTextures.Add(new BufferTextureBinding(texture, address, size, bindingInfo, format, isImage, textureId));
+        }
+
         /// <summary>
         /// Force all bound textures and images to be rebound the next time CommitBindings is called.
         /// </summary>
diff --git a/src/Ryujinx.Graphics.Gpu/Memory/BufferTextureBinding.cs b/src/Ryujinx.Graphics.Gpu/Memory/BufferTextureBinding.cs
index b7a0e7264..29457ae92 100644
--- a/src/Ryujinx.Graphics.Gpu/Memory/BufferTextureBinding.cs
+++ b/src/Ryujinx.Graphics.Gpu/Memory/BufferTextureBinding.cs
@@ -44,6 +44,16 @@ namespace Ryujinx.Graphics.Gpu.Memory
         /// </summary>
         public bool IsImage { get; }
 
+        /// <summary>
+        /// Indicates if the texture should be bound as a bindless texture.
+        /// </summary>
+        public bool AsBindless { get; }
+
+        /// <summary>
+        /// For bindless textures, indicates the texture ID.
+        /// </summary>
+        public int TextureId { get; }
+
         /// <summary>
         /// Create a new buffer texture binding.
         /// </summary>
@@ -70,6 +80,31 @@ namespace Ryujinx.Graphics.Gpu.Memory
             BindingInfo = bindingInfo;
             Format = format;
             IsImage = isImage;
+            AsBindless = false;
+            TextureId = 0;
+        }
+
+        /// <summary>
+        /// Create a new bindless buffer texture binding.
+        /// </summary>
+        /// <param name="texture">Buffer texture</param>
+        /// <param name="address">Base address</param>
+        /// <param name="size">Size in bytes</param>
+        /// <param name="bindingInfo">Binding info</param>
+        /// <param name="format">Binding format</param>
+        /// <param name="isImage">Whether the binding is for an image or a sampler</param>
+        /// <param name="textureId">ID of the texture on the pool</param>
+        public BufferTextureBinding(ITexture texture, ulong address, ulong size, TextureBindingInfo bindingInfo, Format format, bool isImage, int textureId)
+        {
+            Stage = default;
+            Texture = texture;
+            Address = address;
+            Size = size;
+            BindingInfo = bindingInfo;
+            Format = format;
+            IsImage = isImage;
+            AsBindless = true;
+            TextureId = textureId;
         }
     }
 }
diff --git a/src/Ryujinx.Graphics.Gpu/Shader/CachedShaderBindings.cs b/src/Ryujinx.Graphics.Gpu/Shader/CachedShaderBindings.cs
index 1734f08a2..e18ff1f17 100644
--- a/src/Ryujinx.Graphics.Gpu/Shader/CachedShaderBindings.cs
+++ b/src/Ryujinx.Graphics.Gpu/Shader/CachedShaderBindings.cs
@@ -17,6 +17,8 @@ namespace Ryujinx.Graphics.Gpu.Shader
         public BufferDescriptor[][] ConstantBufferBindings { get; }
         public BufferDescriptor[][] StorageBufferBindings { get; }
 
+        public BindlessTextureFlags[] BindlessTextureFlags { get; }
+
         public int MaxTextureBinding { get; }
         public int MaxImageBinding { get; }
 
@@ -34,6 +36,8 @@ namespace Ryujinx.Graphics.Gpu.Shader
             ConstantBufferBindings = new BufferDescriptor[stageCount][];
             StorageBufferBindings = new BufferDescriptor[stageCount][];
 
+            BindlessTextureFlags = new BindlessTextureFlags[stageCount];
+
             int maxTextureBinding = -1;
             int maxImageBinding = -1;
             int offset = isCompute ? 0 : 1;
@@ -94,6 +98,8 @@ namespace Ryujinx.Graphics.Gpu.Shader
 
                 ConstantBufferBindings[i] = stage.Info.CBuffers.ToArray();
                 StorageBufferBindings[i] = stage.Info.SBuffers.ToArray();
+
+                BindlessTextureFlags[i] = stage.Info.BindlessTextureFlags;
             }
 
             MaxTextureBinding = maxTextureBinding;
diff --git a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheGpuAccessor.cs b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheGpuAccessor.cs
index de6432bc1..5f8848482 100644
--- a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheGpuAccessor.cs
+++ b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheGpuAccessor.cs
@@ -123,6 +123,14 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
             return _oldSpecState.GetTextureTarget(_stageIndex, handle, cbufSlot).ConvertSamplerType();
         }
 
+        /// <inheritdoc/>
+        public int QueryTextureBufferIndex()
+        {
+            byte textureBufferIndex = _oldSpecState.GetTextureBufferIndex();
+            _newSpecState.RecordTextureBufferIndex(textureBufferIndex);
+            return textureBufferIndex;
+        }
+
         /// <inheritdoc/>
         public bool QueryTextureCoordNormalized(int handle, int cbufSlot)
         {
diff --git a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs
index 0dc4b1a72..4198cb693 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 = 5791;
+        private const uint CodeGenVersion = 3001;
 
         private const string SharedTocFileName = "shared.toc";
         private const string SharedDataFileName = "shared.data";
@@ -184,6 +184,11 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
             /// Indicates if the vertex shader accesses draw parameters.
             /// </summary>
             public bool UsesDrawParameters;
+
+            /// <summary>
+            /// Flags indicating if and how bindless texture accesses were translated for the shader stage.
+            /// </summary>
+            public BindlessTextureFlags BindlessTextureFlags;
         }
 
         private readonly DiskCacheGuestStorage _guestStorage;
@@ -799,6 +804,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
                 textures,
                 images,
                 dataInfo.Stage,
+                dataInfo.BindlessTextureFlags,
                 dataInfo.GeometryVerticesPerPrimitive,
                 dataInfo.GeometryMaxOutputVertices,
                 dataInfo.ThreadsPerInputPrimitive,
@@ -829,6 +835,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
                 TexturesCount = (ushort)info.Textures.Count,
                 ImagesCount = (ushort)info.Images.Count,
                 Stage = info.Stage,
+                BindlessTextureFlags = info.BindlessTextureFlags,
                 GeometryVerticesPerPrimitive = (byte)info.GeometryVerticesPerPrimitive,
                 GeometryMaxOutputVertices = (ushort)info.GeometryMaxOutputVertices,
                 ThreadsPerInputPrimitive = (ushort)info.ThreadsPerInputPrimitive,
diff --git a/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessor.cs b/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessor.cs
index 1d84d0e46..8845d694e 100644
--- a/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessor.cs
+++ b/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessor.cs
@@ -134,6 +134,14 @@ namespace Ryujinx.Graphics.Gpu.Shader
             return GetTextureDescriptor(handle, cbufSlot).UnpackTextureTarget().ConvertSamplerType();
         }
 
+        /// <inheritdoc/>
+        public int QueryTextureBufferIndex()
+        {
+            byte textureBufferIndex = (byte)_state.PoolState.TextureBufferIndex;
+            _state.SpecializationState?.RecordTextureBufferIndex(textureBufferIndex);
+            return textureBufferIndex;
+        }
+
         /// <inheritdoc/>
         public bool QueryTextureCoordNormalized(int handle, int cbufSlot)
         {
diff --git a/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs b/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs
index a5b31363b..8deb83c74 100644
--- a/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs
+++ b/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs
@@ -40,7 +40,10 @@ namespace Ryujinx.Graphics.Gpu.Shader
         /// <param name="vertexAsCompute">Indicates that the vertex shader will be emulated on a compute shader</param>
         public void InitializeReservedCounts(bool tfEnabled, bool vertexAsCompute)
         {
-            ResourceReservationCounts rrc = new(!_context.Capabilities.SupportsTransformFeedback && tfEnabled, vertexAsCompute);
+            ResourceReservationCounts rrc = new(
+                _context.Capabilities.Api,
+                !_context.Capabilities.SupportsTransformFeedback && tfEnabled,
+                vertexAsCompute);
 
             _reservedConstantBuffers = rrc.ReservedConstantBuffers;
             _reservedStorageBuffers = rrc.ReservedStorageBuffers;
diff --git a/src/Ryujinx.Graphics.Gpu/Shader/ShaderInfoBuilder.cs b/src/Ryujinx.Graphics.Gpu/Shader/ShaderInfoBuilder.cs
index c2258026c..da775e703 100644
--- a/src/Ryujinx.Graphics.Gpu/Shader/ShaderInfoBuilder.cs
+++ b/src/Ryujinx.Graphics.Gpu/Shader/ShaderInfoBuilder.cs
@@ -1,5 +1,6 @@
 using Ryujinx.Graphics.GAL;
 using Ryujinx.Graphics.Shader;
+using System;
 using System.Collections.Generic;
 
 namespace Ryujinx.Graphics.Gpu.Shader
@@ -10,11 +11,17 @@ namespace Ryujinx.Graphics.Gpu.Shader
     class ShaderInfoBuilder
     {
         private const int TotalSets = 4;
+        private const int TotalBindlessSets = 9;
 
         private const int UniformSetIndex = 0;
         private const int StorageSetIndex = 1;
         private const int TextureSetIndex = 2;
         private const int ImageSetIndex = 3;
+        private const int BindlessTextureSetIndex = 4;
+        private const int BindlessBufferTextureSetIndex = 5;
+        private const int BindlessSamplerSetIndex = 6;
+        private const int BindlessImageSetIndex = 7;
+        private const int BindlessBufferImageSetIndex = 8;
 
         private const ResourceStages SupportBufferStages =
             ResourceStages.Compute |
@@ -27,17 +34,20 @@ namespace Ryujinx.Graphics.Gpu.Shader
             ResourceStages.TessellationEvaluation |
             ResourceStages.Geometry;
 
+        private const ResourceStages GraphicsStages = VtgStages | ResourceStages.Fragment;
+
         private readonly GpuContext _context;
 
         private int _fragmentOutputMap;
+        private bool _anyBindless;
 
         private readonly int _reservedConstantBuffers;
         private readonly int _reservedStorageBuffers;
         private readonly int _reservedTextures;
         private readonly int _reservedImages;
 
-        private readonly List<ResourceDescriptor>[] _resourceDescriptors;
-        private readonly List<ResourceUsage>[] _resourceUsages;
+        private List<ResourceDescriptor>[] _resourceDescriptors;
+        private List<ResourceUsage>[] _resourceUsages;
 
         /// <summary>
         /// Creates a new shader info builder.
@@ -63,7 +73,10 @@ namespace Ryujinx.Graphics.Gpu.Shader
             AddDescriptor(SupportBufferStages, ResourceType.UniformBuffer, UniformSetIndex, 0, 1);
             AddUsage(SupportBufferStages, ResourceType.UniformBuffer, UniformSetIndex, 0, 1);
 
-            ResourceReservationCounts rrc = new(!context.Capabilities.SupportsTransformFeedback && tfEnabled, vertexAsCompute);
+            ResourceReservationCounts rrc = new(
+                context.Capabilities.Api,
+                !context.Capabilities.SupportsTransformFeedback && tfEnabled,
+                vertexAsCompute);
 
             _reservedConstantBuffers = rrc.ReservedConstantBuffers;
             _reservedStorageBuffers = rrc.ReservedStorageBuffers;
@@ -97,6 +110,30 @@ namespace Ryujinx.Graphics.Gpu.Shader
                 _fragmentOutputMap = info.FragmentOutputMap;
             }
 
+            if (info.BindlessTextureFlags != BindlessTextureFlags.None && !_anyBindless)
+            {
+                Array.Resize(ref _resourceDescriptors, TotalBindlessSets);
+                Array.Resize(ref _resourceUsages, TotalBindlessSets);
+
+                for (int index = TotalSets; index < TotalBindlessSets; index++)
+                {
+                    _resourceDescriptors[index] = new();
+                    _resourceUsages[index] = new();
+                }
+
+                ResourceStages bindlessStages = info.Stage == ShaderStage.Compute ? ResourceStages.Compute : GraphicsStages;
+
+                AddDescriptor(bindlessStages, ResourceType.UniformBuffer, BindlessTextureSetIndex, 0, 1);
+                AddDescriptor(bindlessStages, ResourceType.StorageBuffer, BindlessTextureSetIndex, 1, 1);
+                AddArrayDescriptor(bindlessStages, ResourceType.Texture, BindlessTextureSetIndex, 2, 0);
+                AddArrayDescriptor(bindlessStages, ResourceType.BufferTexture, BindlessBufferTextureSetIndex, 0, 0);
+                AddArrayDescriptor(bindlessStages, ResourceType.Sampler, BindlessSamplerSetIndex, 0, 0);
+                AddArrayDescriptor(bindlessStages, ResourceType.Image, BindlessImageSetIndex, 0, 0);
+                AddArrayDescriptor(bindlessStages, ResourceType.BufferImage, BindlessBufferImageSetIndex, 0, 0);
+
+                _anyBindless = true;
+            }
+
             int stageIndex = GpuAccessorBase.GetStageIndex(info.Stage switch
             {
                 ShaderStage.TessellationControl => 1,
@@ -154,6 +191,19 @@ namespace Ryujinx.Graphics.Gpu.Shader
             }
         }
 
+        /// <summary>
+        /// Adds an array of resource descriptors to the list of descriptors.
+        /// </summary>
+        /// <param name="stages">Shader stages where the resource is used</param>
+        /// <param name="type">Type of the resource</param>
+        /// <param name="setIndex">Descriptor set number where the resource will be bound</param>
+        /// <param name="binding">Binding number where the resource will be bound</param>
+        /// <param name="count">Number of array elements</param>
+        private void AddArrayDescriptor(ResourceStages stages, ResourceType type, int setIndex, int binding, int count)
+        {
+            _resourceDescriptors[setIndex].Add(new ResourceDescriptor(binding, count, type, stages));
+        }
+
         /// <summary>
         /// Adds two interleaved groups of resources to the list of descriptors.
         /// </summary>
@@ -235,10 +285,10 @@ namespace Ryujinx.Graphics.Gpu.Shader
         /// <returns>Shader information</returns>
         public ShaderInfo Build(ProgramPipelineState? pipeline, bool fromCache = false)
         {
-            var descriptors = new ResourceDescriptorCollection[TotalSets];
-            var usages = new ResourceUsageCollection[TotalSets];
+            var descriptors = new ResourceDescriptorCollection[_resourceDescriptors.Length];
+            var usages = new ResourceUsageCollection[_resourceUsages.Length];
 
-            for (int index = 0; index < TotalSets; index++)
+            for (int index = 0; index < _resourceDescriptors.Length; index++)
             {
                 descriptors[index] = new ResourceDescriptorCollection(_resourceDescriptors[index].ToArray().AsReadOnly());
                 usages[index] = new ResourceUsageCollection(_resourceUsages[index].ToArray().AsReadOnly());
@@ -248,11 +298,11 @@ namespace Ryujinx.Graphics.Gpu.Shader
 
             if (pipeline.HasValue)
             {
-                return new ShaderInfo(_fragmentOutputMap, resourceLayout, pipeline.Value, fromCache);
+                return new ShaderInfo(_fragmentOutputMap, _anyBindless, resourceLayout, pipeline.Value, fromCache);
             }
             else
             {
-                return new ShaderInfo(_fragmentOutputMap, resourceLayout, fromCache);
+                return new ShaderInfo(_fragmentOutputMap, _anyBindless, resourceLayout, fromCache);
             }
         }
 
diff --git a/src/Ryujinx.Graphics.Gpu/Shader/ShaderSpecializationState.cs b/src/Ryujinx.Graphics.Gpu/Shader/ShaderSpecializationState.cs
index a41f761bd..565d3db35 100644
--- a/src/Ryujinx.Graphics.Gpu/Shader/ShaderSpecializationState.cs
+++ b/src/Ryujinx.Graphics.Gpu/Shader/ShaderSpecializationState.cs
@@ -30,11 +30,13 @@ namespace Ryujinx.Graphics.Gpu.Shader
         {
             PrimitiveTopology = 1 << 1,
             TransformFeedback = 1 << 3,
+            TextureBufferIndex = 1 << 4,
         }
 
         private QueriedStateFlags _queriedState;
         private bool _compute;
         private byte _constantBufferUsePerStage;
+        private byte _textureBufferIndex;
 
         /// <summary>
         /// Compute engine state.
@@ -323,6 +325,16 @@ namespace Ryujinx.Graphics.Gpu.Shader
             state.Value.CoordNormalized = coordNormalized;
         }
 
+        /// <summary>
+        /// Records the index of the constant buffer with texture handles.
+        /// </summary>
+        /// <param name="index">Index of the constant buffer with texture handles</param>
+        public void RecordTextureBufferIndex(byte index)
+        {
+            _textureBufferIndex = index;
+            _queriedState |= QueriedStateFlags.TextureBufferIndex;
+        }
+
         /// <summary>
         /// Indicates that the format of a given texture was used during the shader translation process.
         /// </summary>
@@ -385,18 +397,30 @@ namespace Ryujinx.Graphics.Gpu.Shader
         /// <param name="stageIndex">Shader stage where the texture is used</param>
         /// <param name="handle">Offset in words of the texture handle on the texture buffer</param>
         /// <param name="cbufSlot">Slot of the texture buffer constant buffer</param>
+        /// <returns>Format of the given texture, and whether that format is a sRGB format</returns>
         public (uint, bool) GetFormat(int stageIndex, int handle, int cbufSlot)
         {
             TextureSpecializationState state = GetTextureSpecState(stageIndex, handle, cbufSlot).Value;
             return (state.Format, state.FormatSrgb);
         }
 
+        /// <summary>
+        /// Gets the index of the constant buffer with texture handles.
+        /// </summary>
+        /// <returns>Index of the constant buffer with texture handles</returns>
+        public byte GetTextureBufferIndex()
+        {
+            // Note: We assume the NVN default if the cache is old and did not store the buffer index.
+            return _queriedState.HasFlag(QueriedStateFlags.TextureBufferIndex) ? _textureBufferIndex : (byte)TextureHandle.NvnTextureBufferIndex;
+        }
+
         /// <summary>
         /// Gets the recorded target of a given texture.
         /// </summary>
         /// <param name="stageIndex">Shader stage where the texture is used</param>
         /// <param name="handle">Offset in words of the texture handle on the texture buffer</param>
         /// <param name="cbufSlot">Slot of the texture buffer constant buffer</param>
+        /// <returns>Target of the given texture</returns>
         public TextureTarget GetTextureTarget(int stageIndex, int handle, int cbufSlot)
         {
             return GetTextureSpecState(stageIndex, handle, cbufSlot).Value.TextureTarget;
@@ -408,6 +432,7 @@ namespace Ryujinx.Graphics.Gpu.Shader
         /// <param name="stageIndex">Shader stage where the texture is used</param>
         /// <param name="handle">Offset in words of the texture handle on the texture buffer</param>
         /// <param name="cbufSlot">Slot of the texture buffer constant buffer</param>
+        /// <returns>Normalization state of the given texture</returns>
         public bool GetCoordNormalized(int stageIndex, int handle, int cbufSlot)
         {
             return GetTextureSpecState(stageIndex, handle, cbufSlot).Value.CoordNormalized;
@@ -799,6 +824,11 @@ namespace Ryujinx.Graphics.Gpu.Shader
                 constantBufferUsePerStageMask &= ~(1 << index);
             }
 
+            if (specState._queriedState.HasFlag(QueriedStateFlags.TextureBufferIndex))
+            {
+                dataReader.Read(ref specState._textureBufferIndex);
+            }
+
             bool hasPipelineState = false;
 
             dataReader.Read(ref hasPipelineState);
@@ -870,6 +900,11 @@ namespace Ryujinx.Graphics.Gpu.Shader
                 constantBufferUsePerStageMask &= ~(1 << index);
             }
 
+            if (_queriedState.HasFlag(QueriedStateFlags.TextureBufferIndex))
+            {
+                dataWriter.Write(ref _textureBufferIndex);
+            }
+
             bool hasPipelineState = PipelineState.HasValue;
 
             dataWriter.Write(ref hasPipelineState);
diff --git a/src/Ryujinx.Graphics.OpenGL/HwCapabilities.cs b/src/Ryujinx.Graphics.OpenGL/HwCapabilities.cs
index cf0b0645c..43a3b22c4 100644
--- a/src/Ryujinx.Graphics.OpenGL/HwCapabilities.cs
+++ b/src/Ryujinx.Graphics.OpenGL/HwCapabilities.cs
@@ -5,6 +5,7 @@ namespace Ryujinx.Graphics.OpenGL
 {
     static class HwCapabilities
     {
+        private static readonly Lazy<bool> _supportsArbBindlessTexture = new Lazy<bool>(() => HasExtension("GL_ARB_bindless_texture"));
         private static readonly Lazy<bool> _supportsAlphaToCoverageDitherControl = new(() => HasExtension("GL_NV_alpha_to_coverage_dither_control"));
         private static readonly Lazy<bool> _supportsAstcCompression = new(() => HasExtension("GL_KHR_texture_compression_astc_ldr"));
         private static readonly Lazy<bool> _supportsBlendEquationAdvanced = new(() => HasExtension("GL_NV_blend_equation_advanced"));
@@ -14,6 +15,7 @@ namespace Ryujinx.Graphics.OpenGL
         private static readonly Lazy<bool> _supportsGeometryShaderPassthrough = new(() => HasExtension("GL_NV_geometry_shader_passthrough"));
         private static readonly Lazy<bool> _supportsImageLoadFormatted = new(() => HasExtension("GL_EXT_shader_image_load_formatted"));
         private static readonly Lazy<bool> _supportsIndirectParameters = new(() => HasExtension("GL_ARB_indirect_parameters"));
+        private static readonly Lazy<bool> _supportsNvBindlessTexture = new Lazy<bool>(() => HasExtension("GL_NV_bindless_texture"));
         private static readonly Lazy<bool> _supportsParallelShaderCompile = new(() => HasExtension("GL_ARB_parallel_shader_compile"));
         private static readonly Lazy<bool> _supportsPolygonOffsetClamp = new(() => HasExtension("GL_EXT_polygon_offset_clamp"));
         private static readonly Lazy<bool> _supportsQuads = new(SupportsQuadsCheck);
@@ -51,6 +53,7 @@ namespace Ryujinx.Graphics.OpenGL
 
         public static bool UsePersistentBufferForFlush => _gpuVendor.Value == GpuVendor.AmdWindows || _gpuVendor.Value == GpuVendor.Nvidia;
 
+        public static bool SupportsArbBindlessTexture => _supportsArbBindlessTexture.Value;
         public static bool SupportsAlphaToCoverageDitherControl => _supportsAlphaToCoverageDitherControl.Value;
         public static bool SupportsAstcCompression => _supportsAstcCompression.Value;
         public static bool SupportsBlendEquationAdvanced => _supportsBlendEquationAdvanced.Value;
@@ -60,6 +63,7 @@ namespace Ryujinx.Graphics.OpenGL
         public static bool SupportsGeometryShaderPassthrough => _supportsGeometryShaderPassthrough.Value;
         public static bool SupportsImageLoadFormatted => _supportsImageLoadFormatted.Value;
         public static bool SupportsIndirectParameters => _supportsIndirectParameters.Value;
+        public static bool SupportsNvBindlessTexture => _supportsNvBindlessTexture.Value;
         public static bool SupportsParallelShaderCompile => _supportsParallelShaderCompile.Value;
         public static bool SupportsPolygonOffsetClamp => _supportsPolygonOffsetClamp.Value;
         public static bool SupportsQuads => _supportsQuads.Value;
diff --git a/src/Ryujinx.Graphics.OpenGL/Image/BindlessHandleManager.cs b/src/Ryujinx.Graphics.OpenGL/Image/BindlessHandleManager.cs
new file mode 100644
index 000000000..e1239f407
--- /dev/null
+++ b/src/Ryujinx.Graphics.OpenGL/Image/BindlessHandleManager.cs
@@ -0,0 +1,245 @@
+using Ryujinx.Graphics.GAL;
+using Ryujinx.Graphics.Shader;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+
+namespace Ryujinx.Graphics.OpenGL.Image
+{
+    /// <summary>
+    /// Host bindless texture handle manager.
+    /// </summary>
+    class BindlessHandleManager
+    {
+        // This uses two tables to store the handles.
+        // The first level has a fixed size, and stores indices pointing into the second level.
+        // The second level is dynamically allocated, and stores the host handles themselves (among other things).
+        //
+        // The first level is indexed using the low bits of the (guest) texture ID and sampler ID.
+        // The second level can be thought as a 2D array, where the first dimension is indexed using the index from
+        // the first level, and the second dimension is indexed using the high texture ID and sampler ID bits.
+
+        private const int BlockSize = 0x10000;
+
+        private readonly BitMap _freeList;
+
+        /// <summary>
+        /// Second level block state.
+        /// </summary>
+        private struct Block
+        {
+            public int Index;
+            public int ReferenceCount;
+        }
+
+        private readonly Block[] _blocks;
+
+        private readonly Dictionary<int, List<int>> _texturesOnBlocks;
+
+        /// <summary>
+        /// Handle entry accessed by the shader.
+        /// </summary>
+        private struct HandleEntry
+        {
+            public long Handle;
+            public float Scale;
+            public uint Padding;
+
+            public HandleEntry(long handle, float scale)
+            {
+                Handle = handle;
+                Scale = scale;
+                Padding = 0;
+            }
+        }
+
+        private readonly TypedBuffer<int> _textureList; // First level.
+        private readonly TypedBuffer<HandleEntry> _handleList; // Second level.
+
+        private readonly ITexture _bufferTextureForTextureList;
+        private readonly ITexture _bufferTextureForHandleList;
+
+        /// <summary>
+        /// Creates a new instance of the host bindless texture handle manager.
+        /// </summary>
+        /// <param name="renderer">Renderer</param>
+        public BindlessHandleManager(OpenGLRenderer renderer)
+        {
+            _freeList = new BitMap();
+            _blocks = new Block[0x100000];
+            _texturesOnBlocks = new Dictionary<int, List<int>>();
+
+            _textureList = new TypedBuffer<int>(renderer, 0x100000);
+            _handleList = new TypedBuffer<HandleEntry>(renderer, BlockSize);
+
+            _bufferTextureForTextureList = CreateBufferTexture(renderer, _textureList);
+            _bufferTextureForHandleList = CreateBufferTexture(renderer, _handleList);
+        }
+
+        /// <summary>
+        /// Creates a buffer texture with the provided buffer.
+        /// </summary>
+        /// <typeparam name="T">Type of the data on the buffer</typeparam>
+        /// <param name="renderer">Renderer</param>
+        /// <param name="buffer">Buffer</param>
+        /// <returns>Buffer texture</returns>
+        private static ITexture CreateBufferTexture<T>(OpenGLRenderer renderer, TypedBuffer<T> buffer) where T : unmanaged
+        {
+            int bytesPerPixel = Unsafe.SizeOf<T>();
+
+            Format format = bytesPerPixel switch
+            {
+                1 => Format.R8Uint,
+                2 => Format.R16Uint,
+                4 => Format.R32Uint,
+                8 => Format.R32G32Uint,
+                16 => Format.R32G32B32A32Uint,
+                _ => throw new ArgumentException("Invalid type specified.")
+            };
+
+            ITexture texture = renderer.CreateTexture(new TextureCreateInfo(
+                buffer.Size / bytesPerPixel,
+                1,
+                1,
+                1,
+                1,
+                1,
+                1,
+                bytesPerPixel,
+                format,
+                DepthStencilMode.Depth,
+                Target.TextureBuffer,
+                SwizzleComponent.Red,
+                SwizzleComponent.Green,
+                SwizzleComponent.Blue,
+                SwizzleComponent.Alpha));
+
+            texture.SetStorage(buffer.GetBufferRange());
+
+            return texture;
+        }
+
+        /// <summary>
+        /// Binds the multi-level handle table buffer textures on the host.
+        /// </summary>
+        /// <param name="renderer">Renderer</param>
+        public void Bind(OpenGLRenderer renderer)
+        {
+            // TODO: Proper shader stage (doesn't really matter as the OpenGL backend doesn't use this at all).
+            renderer.Pipeline.SetTextureAndSampler(ShaderStage.Vertex, 0, _bufferTextureForTextureList, null);
+            renderer.Pipeline.SetTextureAndSampler(ShaderStage.Vertex, 1, _bufferTextureForHandleList, null);
+        }
+
+        /// <summary>
+        /// Adds a new host handle to the table.
+        /// </summary>
+        /// <param name="textureId">Guest ID of the texture the handle belongs to</param>
+        /// <param name="samplerId">Guest ID of the sampler the handle belongs to</param>
+        /// <param name="handle">Host handle</param>
+        /// <param name="scale">Texture scale factor</param>
+        public void AddBindlessHandle(int textureId, int samplerId, long handle, float scale)
+        {
+            int tableIndex = GetTableIndex(textureId, samplerId);
+            int blockIndex = GetBlockIndex(tableIndex);
+            int subIndex = GetSubIndex(textureId, samplerId);
+
+            _blocks[tableIndex].ReferenceCount++;
+
+            if (!_texturesOnBlocks.TryGetValue(subIndex, out List<int> list))
+            {
+                _texturesOnBlocks.Add(subIndex, list = new List<int>());
+            }
+
+            list.Add(tableIndex);
+
+            if (_handleList.EnsureCapacity((blockIndex + 1) * BlockSize))
+            {
+                _bufferTextureForHandleList.SetStorage(_handleList.GetBufferRange());
+            }
+
+            _handleList.Write(blockIndex * BlockSize + subIndex, new HandleEntry(handle, scale));
+        }
+
+        /// <summary>
+        /// Removes a handle from the table.
+        /// </summary>
+        /// <param name="textureId">Guest ID of the texture the handle belongs to</param>
+        /// <param name="samplerId">Guest ID of the sampler the handle belongs to</param>
+        public void RemoveBindlessHandle(int textureId, int samplerId)
+        {
+            int tableIndex = GetTableIndex(textureId, samplerId);
+            int blockIndex = _blocks[tableIndex].Index - 1;
+            int subIndex = GetSubIndex(textureId, samplerId);
+
+            Debug.Assert(blockIndex >= 0);
+
+            _handleList.Write(blockIndex * BlockSize + subIndex, new HandleEntry(0L, 0f));
+
+            if (_texturesOnBlocks.TryGetValue(subIndex, out List<int> list))
+            {
+                for (int i = 0; i < list.Count; i++)
+                {
+                    PutBlockIndex(list[i]);
+                }
+
+                _texturesOnBlocks.Remove(subIndex);
+            }
+        }
+
+        /// <summary>
+        /// Gets a index, pointing inside the second level table, from the first level table.
+        /// This will dynamically allocate a new block on the second level if needed, and write
+        /// its index into the first level.
+        /// </summary>
+        /// <param name="tableIndex">Index pointing inside the first level table, where the other index is located</param>
+        /// <returns>The index of a block on the second level table</returns>
+        private int GetBlockIndex(int tableIndex)
+        {
+            if (_blocks[tableIndex].Index != 0)
+            {
+                return _blocks[tableIndex].Index - 1;
+            }
+
+            int blockIndex = _freeList.FindFirstUnset();
+
+            _freeList.Set(blockIndex);
+
+            _blocks[tableIndex].Index = blockIndex + 1;
+
+            _textureList.Write(tableIndex, blockIndex);
+
+            return blockIndex;
+        }
+
+        /// <summary>
+        /// Indicates that a given block was dereferenced, eventually freeing it if no longer in use.
+        /// </summary>
+        /// <param name="tableIndex">Index of the block index on the first level table</param>
+        private void PutBlockIndex(int tableIndex)
+        {
+            if (--_blocks[tableIndex].ReferenceCount == 0)
+            {
+                _freeList.Clear(_blocks[tableIndex].Index - 1);
+
+                _blocks[tableIndex].Index = 0;
+            }
+        }
+
+        /// <summary>
+        /// Assembles a index from the low bits of the texture and sampler ID, used for the first level indexing.
+        /// </summary>
+        /// <param name="textureId">Texture ID</param>
+        /// <param name="samplerId">Sampler ID</param>
+        /// <returns>The first level index</returns>
+        private static int GetTableIndex(int textureId, int samplerId) => (textureId >> 8) | ((samplerId >> 8) << 12);
+
+        /// <summary>
+        /// Assembles a index from the low bits of the texture and sampler ID, used for the second level indexing.
+        /// </summary>
+        /// <param name="textureId">Texture ID</param>
+        /// <param name="samplerId">Sampler ID</param>
+        /// <returns>The second level index</returns>
+        private static int GetSubIndex(int textureId, int samplerId) => (textureId & 0xff) | ((samplerId & 0xff) << 8);
+    }
+}
diff --git a/src/Ryujinx.Graphics.OpenGL/Image/BindlessManager.cs b/src/Ryujinx.Graphics.OpenGL/Image/BindlessManager.cs
new file mode 100644
index 000000000..773cc1a99
--- /dev/null
+++ b/src/Ryujinx.Graphics.OpenGL/Image/BindlessManager.cs
@@ -0,0 +1,166 @@
+using OpenTK.Graphics.OpenGL;
+using Ryujinx.Graphics.GAL;
+using System.Collections.Generic;
+
+namespace Ryujinx.Graphics.OpenGL.Image
+{
+    /// <summary>
+    /// Host bindless texture manager.
+    /// </summary>
+    class BindlessManager
+    {
+        private readonly OpenGLRenderer _renderer;
+        private BindlessHandleManager _handleManager;
+        private readonly Dictionary<int, (ITexture, float)> _separateTextures;
+        private readonly Dictionary<int, ISampler> _separateSamplers;
+
+        private readonly HashSet<long> _handles = new HashSet<long>();
+
+        public BindlessManager(OpenGLRenderer renderer)
+        {
+            _renderer = renderer;
+            _separateTextures = new();
+            _separateSamplers = new();
+        }
+
+        public void AddSeparateSampler(int samplerId, ISampler sampler)
+        {
+            _separateSamplers[samplerId] = sampler;
+
+            foreach ((int textureId, (ITexture texture, float textureScale)) in _separateTextures)
+            {
+                Add(textureId, texture, textureScale, samplerId, sampler);
+            }
+        }
+
+        public void AddSeparateTexture(int textureId, ITexture texture, float textureScale)
+        {
+            _separateTextures[textureId] = (texture, textureScale);
+
+            bool hasDeletedSamplers = false;
+
+            foreach ((int samplerId, ISampler sampler) in _separateSamplers)
+            {
+                if ((sampler as Sampler).Handle == 0)
+                {
+                    hasDeletedSamplers = true;
+                    continue;
+                }
+
+                Add(textureId, texture, textureScale, samplerId, sampler);
+            }
+
+            if (hasDeletedSamplers)
+            {
+                List<int> toRemove = new List<int>();
+
+                foreach ((int samplerId, ISampler sampler) in _separateSamplers)
+                {
+                    if ((sampler as Sampler).Handle == 0)
+                    {
+                        toRemove.Add(samplerId);
+                    }
+                }
+
+                foreach (int samplerId in toRemove)
+                {
+                    _separateSamplers.Remove(samplerId);
+                }
+            }
+        }
+
+        public void Add(int textureId, ITexture texture, float textureScale, int samplerId, ISampler sampler)
+        {
+            EnsureHandleManager();
+            Register(textureId, samplerId, texture as TextureBase, sampler as Sampler, textureScale);
+        }
+
+        private void Register(int textureId, int samplerId, TextureBase texture, Sampler sampler, float textureScale)
+        {
+            if (texture == null)
+            {
+                return;
+            }
+
+            long bindlessHandle = sampler != null
+                ? GetTextureSamplerHandle(texture.Handle, sampler.Handle)
+                : GetTextureHandle(texture.Handle);
+
+            if (bindlessHandle != 0 && texture.AddBindlessHandle(textureId, samplerId, this, bindlessHandle))
+            {
+                _handles.Add(bindlessHandle);
+                MakeTextureHandleResident(bindlessHandle);
+                _handleManager.AddBindlessHandle(textureId, samplerId, bindlessHandle, textureScale);
+            }
+        }
+
+        public void Unregister(int textureId, int samplerId, long bindlessHandle)
+        {
+            _handleManager.RemoveBindlessHandle(textureId, samplerId);
+            MakeTextureHandleNonResident(bindlessHandle);
+            _handles.Remove(bindlessHandle);
+            _separateTextures.Remove(textureId);
+        }
+
+        private void EnsureHandleManager()
+        {
+            if (_handleManager == null)
+            {
+                _handleManager = new BindlessHandleManager(_renderer);
+                _handleManager.Bind(_renderer);
+            }
+        }
+
+        private static long GetTextureHandle(int texture)
+        {
+            if (HwCapabilities.SupportsNvBindlessTexture)
+            {
+                return GL.NV.GetTextureHandle(texture);
+            }
+            else if (HwCapabilities.SupportsArbBindlessTexture)
+            {
+                return GL.Arb.GetTextureHandle(texture);
+            }
+
+            return 0;
+        }
+
+        private static long GetTextureSamplerHandle(int texture, int sampler)
+        {
+            if (HwCapabilities.SupportsNvBindlessTexture)
+            {
+                return GL.NV.GetTextureSamplerHandle(texture, sampler);
+            }
+            else if (HwCapabilities.SupportsArbBindlessTexture)
+            {
+                return GL.Arb.GetTextureSamplerHandle(texture, sampler);
+            }
+
+            return 0;
+        }
+
+        private static void MakeTextureHandleResident(long handle)
+        {
+            if (HwCapabilities.SupportsNvBindlessTexture)
+            {
+                GL.NV.MakeTextureHandleResident(handle);
+            }
+            else if (HwCapabilities.SupportsArbBindlessTexture)
+            {
+                GL.Arb.MakeTextureHandleResident(handle);
+            }
+        }
+
+        private static void MakeTextureHandleNonResident(long handle)
+        {
+            if (HwCapabilities.SupportsNvBindlessTexture)
+            {
+                GL.NV.MakeTextureHandleNonResident(handle);
+            }
+            else if (HwCapabilities.SupportsArbBindlessTexture)
+            {
+                GL.Arb.MakeTextureHandleNonResident(handle);
+            }
+        }
+    }
+}
diff --git a/src/Ryujinx.Graphics.OpenGL/Image/BitMap.cs b/src/Ryujinx.Graphics.OpenGL/Image/BitMap.cs
new file mode 100644
index 000000000..a3a445e24
--- /dev/null
+++ b/src/Ryujinx.Graphics.OpenGL/Image/BitMap.cs
@@ -0,0 +1,189 @@
+using System.Collections.Generic;
+using System.Numerics;
+
+namespace Ryujinx.Graphics.OpenGL.Image
+{
+    /// <summary>
+    /// Represents a list of bits.
+    /// </summary>
+    class BitMap
+    {
+        private const int IntSize = 64;
+        private const int IntMask = IntSize - 1;
+
+        private readonly List<ulong> _masks;
+
+        /// <summary>
+        /// Creates a new instance of the bitmap.
+        /// </summary>
+        public BitMap()
+        {
+            _masks = new List<ulong>(0);
+        }
+
+        /// <summary>
+        /// Creates a new instance of the bitmap.
+        /// </summary>
+        /// <param name="initialCapacity">Initial size (in bits) that the bitmap can hold</param>
+        public BitMap(int initialCapacity)
+        {
+            int count = (initialCapacity + IntMask) / IntSize;
+
+            _masks = new List<ulong>(count);
+
+            while (count-- > 0)
+            {
+                _masks.Add(0);
+            }
+        }
+
+        /// <summary>
+        /// Sets a bit on the list to 1.
+        /// </summary>
+        /// <param name="bit">Index of the bit</param>
+        /// <returns>True if the bit value was modified by this operation, false otherwise</returns>
+        public bool Set(int bit)
+        {
+            EnsureCapacity(bit + 1);
+
+            int wordIndex = bit / IntSize;
+            int wordBit   = bit & IntMask;
+
+            ulong wordMask = 1UL << wordBit;
+
+            if ((_masks[wordIndex] & wordMask) != 0)
+            {
+                return false;
+            }
+
+            _masks[wordIndex] |= wordMask;
+
+            return true;
+        }
+
+        /// <summary>
+        /// Sets a bit on the list to 0.
+        /// </summary>
+        /// <param name="bit">Index of the bit</param>
+        public void Clear(int bit)
+        {
+            EnsureCapacity(bit + 1);
+
+            int wordIndex = bit / IntSize;
+            int wordBit   = bit & IntMask;
+
+            ulong wordMask = 1UL << wordBit;
+
+            _masks[wordIndex] &= ~wordMask;
+        }
+
+        /// <summary>
+        /// Finds the first bit on the list with a value of 0.
+        /// </summary>
+        /// <returns>Index of the bit with value 0</returns>
+        public int FindFirstUnset()
+        {
+            int index = 0;
+
+            while (index < _masks.Count && _masks[index] == ulong.MaxValue)
+            {
+                index++;
+            }
+
+            if (index == _masks.Count)
+            {
+                _masks.Add(0);
+            }
+
+            int bit = index * IntSize;
+
+            bit += BitOperations.TrailingZeroCount(~_masks[index]);
+
+            return bit;
+        }
+
+        /// <summary>
+        /// Ensures that the array can hold a given number of bits, resizing as needed.
+        /// </summary>
+        /// <param name="size">Number of bits</param>
+        private void EnsureCapacity(int size)
+        {
+            while (_masks.Count * IntSize < size)
+            {
+                _masks.Add(0);
+            }
+        }
+
+        private int _iterIndex;
+        private ulong _iterMask;
+
+        /// <summary>
+        /// Starts iterating from bit 0.
+        /// </summary>
+        public void BeginIterating()
+        {
+            _iterIndex = 0;
+            _iterMask = _masks.Count != 0 ? _masks[0] : 0;
+        }
+
+        /// <summary>
+        /// Gets the next bit set to 1 on the list.
+        /// </summary>
+        /// <returns>Index of the bit, or -1 if none found</returns>
+        public int GetNext()
+        {
+            if (_iterIndex >= _masks.Count)
+            {
+                return -1;
+            }
+
+            while (_iterMask == 0 && _iterIndex + 1 < _masks.Count)
+            {
+                _iterMask = _masks[++_iterIndex];
+            }
+
+            if (_iterMask == 0)
+            {
+                return -1;
+            }
+
+            int bit = BitOperations.TrailingZeroCount(_iterMask);
+
+            _iterMask &= ~(1UL << bit);
+
+            return _iterIndex * IntSize + bit;
+        }
+
+        /// <summary>
+        /// Gets the next bit set to 1 on the list, while also setting it to 0.
+        /// </summary>
+        /// <returns>Index of the bit, or -1 if none found</returns>
+        public int GetNextAndClear()
+        {
+            if (_iterIndex >= _masks.Count)
+            {
+                return -1;
+            }
+
+            ulong mask = _masks[_iterIndex];
+
+            while (mask == 0 && _iterIndex + 1 < _masks.Count)
+            {
+                mask = _masks[++_iterIndex];
+            }
+
+            if (mask == 0)
+            {
+                return -1;
+            }
+
+            int bit = BitOperations.TrailingZeroCount(mask);
+
+            mask &= ~(1UL << bit);
+
+            _masks[_iterIndex] = mask;
+
+            return _iterIndex * IntSize + bit;
+        }
+    }
+}
diff --git a/src/Ryujinx.Graphics.OpenGL/Image/TextureBase.cs b/src/Ryujinx.Graphics.OpenGL/Image/TextureBase.cs
index 070a36b5e..6fa785a7e 100644
--- a/src/Ryujinx.Graphics.OpenGL/Image/TextureBase.cs
+++ b/src/Ryujinx.Graphics.OpenGL/Image/TextureBase.cs
@@ -1,5 +1,6 @@
 using OpenTK.Graphics.OpenGL;
 using Ryujinx.Graphics.GAL;
+using System.Collections.Generic;
 
 namespace Ryujinx.Graphics.OpenGL.Image
 {
@@ -15,6 +16,8 @@ namespace Ryujinx.Graphics.OpenGL.Image
         public Target Target => Info.Target;
         public Format Format => Info.Format;
 
+        private Dictionary<int, (BindlessManager, long, int)> _bindlessHandles;
+
         public TextureBase(TextureCreateInfo info)
         {
             Info = info;
@@ -35,8 +38,32 @@ namespace Ryujinx.Graphics.OpenGL.Image
 
         public static void ClearBinding(int unit)
         {
-            GL.ActiveTexture(TextureUnit.Texture0 + unit);
             GL.BindTextureUnit(unit, 0);
         }
+
+        public bool AddBindlessHandle(int textureId, int samplerId, BindlessManager owner, long bindlessHandle)
+        {
+            var bindlessHandles = _bindlessHandles ??= new();
+            return bindlessHandles.TryAdd(samplerId, (owner, bindlessHandle, textureId));
+        }
+
+        public void RevokeBindlessAccess()
+        {
+            if (_bindlessHandles == null)
+            {
+                return;
+            }
+
+            foreach (var kv in _bindlessHandles)
+            {
+                int samplerId = kv.Key;
+                (BindlessManager owner, long bindlessHandle, int textureId) = kv.Value;
+
+                owner.Unregister(textureId, samplerId, bindlessHandle);
+            }
+
+            _bindlessHandles.Clear();
+            _bindlessHandles = null;
+        }
     }
 }
diff --git a/src/Ryujinx.Graphics.OpenGL/Image/TextureBuffer.cs b/src/Ryujinx.Graphics.OpenGL/Image/TextureBuffer.cs
index c177ae9c6..40d4764a1 100644
--- a/src/Ryujinx.Graphics.OpenGL/Image/TextureBuffer.cs
+++ b/src/Ryujinx.Graphics.OpenGL/Image/TextureBuffer.cs
@@ -97,6 +97,8 @@ namespace Ryujinx.Graphics.OpenGL.Image
 
         public void Dispose()
         {
+            RevokeBindlessAccess();
+
             if (Handle != 0)
             {
                 GL.DeleteTexture(Handle);
diff --git a/src/Ryujinx.Graphics.OpenGL/Image/TextureView.cs b/src/Ryujinx.Graphics.OpenGL/Image/TextureView.cs
index 7f1b1c382..c17dba09c 100644
--- a/src/Ryujinx.Graphics.OpenGL/Image/TextureView.cs
+++ b/src/Ryujinx.Graphics.OpenGL/Image/TextureView.cs
@@ -890,6 +890,8 @@ namespace Ryujinx.Graphics.OpenGL.Image
         /// </summary>
         public void Release()
         {
+            RevokeBindlessAccess();
+
             bool hadHandle = Handle != 0;
 
             if (_parent.DefaultView != this)
diff --git a/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs b/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs
index 667ea7825..6c97274c0 100644
--- a/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs
+++ b/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs
@@ -45,7 +45,7 @@ namespace Ryujinx.Graphics.OpenGL
 
         public OpenGLRenderer()
         {
-            _pipeline = new Pipeline();
+            _pipeline = new Pipeline(this);
             _counters = new Counters();
             _window = new Window(this);
             _textureCopy = new TextureCopy(this);
diff --git a/src/Ryujinx.Graphics.OpenGL/Pipeline.cs b/src/Ryujinx.Graphics.OpenGL/Pipeline.cs
index 923c85d7e..75c598943 100644
--- a/src/Ryujinx.Graphics.OpenGL/Pipeline.cs
+++ b/src/Ryujinx.Graphics.OpenGL/Pipeline.cs
@@ -26,7 +26,6 @@ namespace Ryujinx.Graphics.OpenGL
         private IntPtr _indexBaseOffset;
 
         private DrawElementsType _elementsType;
-
         private PrimitiveType _primitiveType;
 
         private int _stencilFrontMask;
@@ -66,9 +65,11 @@ namespace Ryujinx.Graphics.OpenGL
         private readonly BufferHandle[] _tfbs;
         private readonly BufferRange[] _tfbTargets;
 
+        private readonly BindlessManager _bindlessManager;
+
         private ColorF _blendConstant;
 
-        internal Pipeline()
+        internal Pipeline(OpenGLRenderer renderer)
         {
             _drawTexture = new DrawTextureEmulation();
             _rasterizerDiscard = false;
@@ -82,6 +83,8 @@ namespace Ryujinx.Graphics.OpenGL
 
             _tfbs = new BufferHandle[Constants.MaxTransformFeedbackBuffers];
             _tfbTargets = new BufferRange[Constants.MaxTransformFeedbackBuffers];
+
+            _bindlessManager = new BindlessManager(renderer);
         }
 
         public void Barrier()
@@ -759,6 +762,21 @@ namespace Ryujinx.Graphics.OpenGL
             _tfEnabled = false;
         }
 
+        public void RegisterBindlessSampler(int samplerId, ISampler sampler)
+        {
+            _bindlessManager.AddSeparateSampler(samplerId, sampler);
+        }
+
+        public void RegisterBindlessTexture(int textureId, ITexture texture, float textureScale)
+        {
+            _bindlessManager.AddSeparateTexture(textureId, texture, textureScale);
+        }
+
+        public void RegisterBindlessTextureAndSampler(int textureId, ITexture texture, float textureScale, int samplerId, ISampler sampler)
+        {
+            _bindlessManager.Add(textureId, texture, textureScale, samplerId, sampler);
+        }
+
         public void SetAlphaTest(bool enable, float reference, CompareOp op)
         {
             if (!enable)
@@ -1302,7 +1320,6 @@ namespace Ryujinx.Graphics.OpenGL
             }
         }
 
-
         public void SetTransformFeedbackBuffers(ReadOnlySpan<BufferRange> buffers)
         {
             if (_tfEnabled)
diff --git a/src/Ryujinx.Graphics.OpenGL/TypedBuffer.cs b/src/Ryujinx.Graphics.OpenGL/TypedBuffer.cs
new file mode 100644
index 000000000..497d1c522
--- /dev/null
+++ b/src/Ryujinx.Graphics.OpenGL/TypedBuffer.cs
@@ -0,0 +1,79 @@
+using Ryujinx.Graphics.GAL;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Graphics.OpenGL
+{
+    /// <summary>
+    /// Represents a buffer that stores data of a given type.
+    /// </summary>
+    /// <typeparam name="T">Type of the buffer data</typeparam>
+    class TypedBuffer<T> where T : unmanaged
+    {
+        private readonly OpenGLRenderer _renderer;
+        private BufferHandle _buffer;
+
+        /// <summary>
+        /// Size of the buffer in bytes.
+        /// </summary>
+        public int Size { get; private set; }
+
+        /// <summary>
+        /// Creates a new instance of the typed buffer.
+        /// </summary>
+        /// <param name="renderer">Renderer</param>
+        /// <param name="count">Number of data elements on the buffer</param>
+        public TypedBuffer(OpenGLRenderer renderer, int count)
+        {
+            _renderer = renderer;
+            _buffer = renderer.CreateBuffer(Size = count * Unsafe.SizeOf<T>(), BufferHandle.Null);
+            renderer.SetBufferData(_buffer, 0, new byte[Size]);
+        }
+
+        /// <summary>
+        /// Writes data into a given buffer index.
+        /// </summary>
+        /// <param name="index">Index to write the data</param>
+        /// <param name="value">Data to be written</param>
+        public void Write(int index, T value)
+        {
+            _renderer.SetBufferData(_buffer, index * Unsafe.SizeOf<T>(), MemoryMarshal.Cast<T, byte>(MemoryMarshal.CreateSpan(ref value, 1)));
+        }
+
+        /// <summary>
+        /// Ensures that the buffer can hold a given number of elements.
+        /// </summary>
+        /// <param name="count">Number of elements</param>
+        /// <returns>True if the buffer was resized and needs to be rebound, false otherwise</returns>
+        public bool EnsureCapacity(int count)
+        {
+            int size = count * Unsafe.SizeOf<T>();
+
+            if (Size < size)
+            {
+                BufferHandle oldBuffer = _buffer;
+                BufferHandle newBuffer = _renderer.CreateBuffer(size, BufferHandle.Null);
+                _renderer.SetBufferData(newBuffer, 0, new byte[size]);
+
+                _renderer.Pipeline.CopyBuffer(oldBuffer, newBuffer, 0, 0, Size);
+                _renderer.DeleteBuffer(oldBuffer);
+
+                _buffer = newBuffer;
+                Size = size;
+
+                return true;
+            }
+
+            return false;
+        }
+
+        /// <summary>
+        /// Gets a buffer range covering the whole buffer.
+        /// </summary>
+        /// <returns>The buffer range</returns>
+        public BufferRange GetBufferRange()
+        {
+            return new BufferRange(_buffer, 0, Size);
+        }
+    }
+}
diff --git a/src/Ryujinx.Graphics.Shader/BindlessTextureFlags.cs b/src/Ryujinx.Graphics.Shader/BindlessTextureFlags.cs
new file mode 100644
index 000000000..005759532
--- /dev/null
+++ b/src/Ryujinx.Graphics.Shader/BindlessTextureFlags.cs
@@ -0,0 +1,11 @@
+namespace Ryujinx.Graphics.Shader
+{
+    public enum BindlessTextureFlags : ushort
+    {
+        None = 0,
+
+        BindlessConverted = 1 << 0,
+        BindlessNvn = 1 << 1,
+        BindlessFull = 1 << 2,
+    }
+}
\ No newline at end of file
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/CodeGenParameters.cs b/src/Ryujinx.Graphics.Shader/CodeGen/CodeGenParameters.cs
index f692c428b..727ecab35 100644
--- a/src/Ryujinx.Graphics.Shader/CodeGen/CodeGenParameters.cs
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/CodeGenParameters.cs
@@ -11,6 +11,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen
         public readonly HostCapabilities HostCapabilities;
         public readonly ILogger Logger;
         public readonly TargetApi TargetApi;
+        public readonly BindlessTextureFlags BindlessTextureFlags;
 
         public CodeGenParameters(
             AttributeUsage attributeUsage,
@@ -18,7 +19,8 @@ namespace Ryujinx.Graphics.Shader.CodeGen
             ShaderProperties properties,
             HostCapabilities hostCapabilities,
             ILogger logger,
-            TargetApi targetApi)
+            TargetApi targetApi,
+            BindlessTextureFlags bindlessTextureFlags)
         {
             AttributeUsage = attributeUsage;
             Definitions = definitions;
@@ -26,6 +28,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen
             HostCapabilities = hostCapabilities;
             Logger = logger;
             TargetApi = targetApi;
+            BindlessTextureFlags = bindlessTextureFlags;
         }
     }
 }
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/CodeGenContext.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/CodeGenContext.cs
index cd9c71280..2bd74b9a8 100644
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/CodeGenContext.cs
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/CodeGenContext.cs
@@ -18,6 +18,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
         public HostCapabilities HostCapabilities { get; }
         public ILogger Logger { get; }
         public TargetApi TargetApi { get; }
+        public BindlessTextureFlags BindlessTextureFlags { get; }
 
         public OperandManager OperandManager { get; }
 
@@ -36,6 +37,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
             HostCapabilities = parameters.HostCapabilities;
             Logger = parameters.Logger;
             TargetApi = parameters.TargetApi;
+            BindlessTextureFlags = parameters.BindlessTextureFlags;
 
             OperandManager = new OperandManager();
 
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Declarations.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Declarations.cs
index 500de71f6..1c3a9ddf6 100644
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Declarations.cs
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Declarations.cs
@@ -6,7 +6,6 @@ using System;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
-using System.Numerics;
 
 namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
 {
@@ -15,6 +14,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
         public static void Declare(CodeGenContext context, StructuredProgramInfo info)
         {
             context.AppendLine(context.TargetApi == TargetApi.Vulkan ? "#version 460 core" : "#version 450 core");
+            context.AppendLine("#extension GL_ARB_bindless_texture : enable");
             context.AppendLine("#extension GL_ARB_gpu_shader_int64 : enable");
 
             if (context.HostCapabilities.SupportsShaderBallot)
@@ -32,6 +32,11 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
             context.AppendLine("#extension GL_EXT_shader_image_load_formatted : enable");
             context.AppendLine("#extension GL_EXT_texture_shadow_lod : enable");
 
+            if (context.TargetApi == TargetApi.Vulkan)
+            {
+                context.AppendLine("#extension GL_EXT_nonuniform_qualifier : enable");
+            }
+
             if (context.Definitions.Stage == ShaderStage.Compute)
             {
                 context.AppendLine("#extension GL_ARB_compute_shader : enable");
@@ -189,6 +194,18 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
                 }
             }
 
+            if (context.BindlessTextureFlags != BindlessTextureFlags.None)
+            {
+                if (context.TargetApi == TargetApi.Vulkan)
+                {
+                    AppendHelperFunction(context, "Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/GetBindlessHandleVk.glsl");
+                }
+                else
+                {
+                    AppendHelperFunction(context, "Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/GetBindlessHandle.glsl");
+                }
+            }
+
             if ((info.HelperFunctionsMask & HelperFunctionsMask.MultiplyHighS32) != 0)
             {
                 AppendHelperFunction(context, "Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/MultiplyHighS32.glsl");
@@ -337,83 +354,84 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
             }
         }
 
-        private static void DeclareSamplers(CodeGenContext context, IEnumerable<TextureDefinition> definitions)
+        private static void DeclareSamplers(CodeGenContext context, IEnumerable<TextureDefinition> samplers)
         {
-            int arraySize = 0;
-
-            foreach (var definition in definitions)
+            foreach (var sampler in samplers)
             {
-                string indexExpr = string.Empty;
-
-                if (definition.Type.HasFlag(SamplerType.Indexed))
-                {
-                    if (arraySize == 0)
-                    {
-                        arraySize = ResourceManager.SamplerArraySize;
-                    }
-                    else if (--arraySize != 0)
-                    {
-                        continue;
-                    }
-
-                    indexExpr = $"[{NumberFormatter.FormatInt(arraySize)}]";
-                }
-
-                string samplerTypeName = definition.Type.ToGlslSamplerType();
+                string samplerTypeName = sampler.Type.HasFlag(SamplerType.Separate)
+                    ? (sampler.Type & ~SamplerType.Separate).ToGlslTextureType()
+                    : sampler.Type.ToGlslSamplerType();
 
                 string layout = string.Empty;
 
                 if (context.TargetApi == TargetApi.Vulkan)
                 {
-                    layout = $", set = {definition.Set}";
+                    layout = $"set = {sampler.Set}, ";
                 }
 
-                context.AppendLine($"layout (binding = {definition.Binding}{layout}) uniform {samplerTypeName} {definition.Name}{indexExpr};");
+                string suffix = string.Empty;
+
+                if (sampler.ArraySize == 0)
+                {
+                    suffix = "[]";
+                }
+                else if (sampler.ArraySize != 1)
+                {
+                    suffix = $"[{(uint)sampler.ArraySize}]";
+                }
+
+                if (sampler.ArraySize != 1)
+                {
+                    Console.WriteLine($"found bindless {sampler.Name} {(sampler.Type & ~SamplerType.Shadow)}");
+                    context.OperandManager.BindlessTextures[sampler.Type & ~(SamplerType.Shadow | SamplerType.Separate)] = sampler.Name;
+                }
+
+                context.AppendLine($"layout ({layout}binding = {sampler.Binding}) uniform {samplerTypeName} {sampler.Name}{suffix};");
             }
         }
 
-        private static void DeclareImages(CodeGenContext context, IEnumerable<TextureDefinition> definitions)
+        private static void DeclareImages(CodeGenContext context, IEnumerable<TextureDefinition> images)
         {
-            int arraySize = 0;
-
-            foreach (var definition in definitions)
+            foreach (var image in images)
             {
-                string indexExpr = string.Empty;
+                string imageTypeName = image.Type.ToGlslImageType(image.Format.GetComponentType());
 
-                if (definition.Type.HasFlag(SamplerType.Indexed))
-                {
-                    if (arraySize == 0)
-                    {
-                        arraySize = ResourceManager.SamplerArraySize;
-                    }
-                    else if (--arraySize != 0)
-                    {
-                        continue;
-                    }
-
-                    indexExpr = $"[{NumberFormatter.FormatInt(arraySize)}]";
-                }
-
-                string imageTypeName = definition.Type.ToGlslImageType(definition.Format.GetComponentType());
-
-                if (definition.Flags.HasFlag(TextureUsageFlags.ImageCoherent))
+                if (image.Flags.HasFlag(TextureUsageFlags.ImageCoherent))
                 {
                     imageTypeName = "coherent " + imageTypeName;
                 }
 
-                string layout = definition.Format.ToGlslFormat();
-
-                if (!string.IsNullOrEmpty(layout))
-                {
-                    layout = ", " + layout;
-                }
+                string layout = string.Empty;
 
                 if (context.TargetApi == TargetApi.Vulkan)
                 {
-                    layout = $", set = {definition.Set}{layout}";
+                    layout = $"set = {image.Set}, ";
                 }
 
-                context.AppendLine($"layout (binding = {definition.Binding}{layout}) uniform {imageTypeName} {definition.Name}{indexExpr};");
+                string format = image.Format.ToGlslFormat();
+
+                if (!string.IsNullOrEmpty(format))
+                {
+                    format = ", " + format;
+                }
+
+                string suffix = string.Empty;
+
+                if (image.ArraySize == 0)
+                {
+                    suffix = "[]";
+                }
+                else if (image.ArraySize != 1)
+                {
+                    suffix = $"[{(uint)image.ArraySize}]";
+                }
+
+                if (image.ArraySize != 1)
+                {
+                    context.OperandManager.BindlessTextures[image.Type] = image.Name;
+                }
+
+                context.AppendLine($"layout ({layout}binding = {image.Binding}{format}) uniform {imageTypeName} {image.Name}{suffix};");
             }
         }
 
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/GetBindlessHandle.glsl b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/GetBindlessHandle.glsl
new file mode 100644
index 000000000..af67453c5
--- /dev/null
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/GetBindlessHandle.glsl
@@ -0,0 +1,22 @@
+layout(binding = 0) uniform usamplerBuffer texture_list;
+layout(binding = 1) uniform usamplerBuffer handle_list;
+
+uvec4 Helper_GetBindlessInfo(int nvHandle)
+{
+    int textureId = nvHandle & 0xfffff;
+    int samplerId = (nvHandle >> 20) & 0xfff;
+    int index = (textureId >> 8) | ((samplerId >> 8) << 12);
+    int subIdx = (textureId & 0xff) | ((samplerId & 0xff) << 8);
+    int hndIdx = int(texelFetch(texture_list, index).x) * 0x10000 + subIdx;
+    return texelFetch(handle_list, hndIdx);
+}
+
+uvec2 Helper_GetBindlessHandle(int nvHandle)
+{
+    return Helper_GetBindlessInfo(nvHandle).xy;
+}
+
+float Helper_GetBindlessScale(int nvHandle)
+{
+    return uintBitsToFloat(Helper_GetBindlessInfo(nvHandle).z);
+}
\ No newline at end of file
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/GetBindlessHandleVk.glsl b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/GetBindlessHandleVk.glsl
new file mode 100644
index 000000000..05cb2ec3f
--- /dev/null
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/GetBindlessHandleVk.glsl
@@ -0,0 +1,16 @@
+uint Helper_GetBindlessTextureIndex(int nvHandle)
+{
+    int id = nvHandle & 0xfffff;
+    return bindless_table[id >> 8].x | uint(id & 0xff);
+}
+
+uint Helper_GetBindlessSamplerIndex(int nvHandle)
+{
+    int id = (nvHandle >> 20) & 0xfff;
+    return bindless_table[id >> 8].y | uint(id & 0xff);
+}
+
+float Helper_GetBindlessScale(int nvHandle)
+{
+    return bindless_scales[Helper_GetBindlessTextureIndex(nvHandle)];
+}
\ No newline at end of file
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/HelperFunctionNames.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/HelperFunctionNames.cs
index 0b80ac2b6..ff0a893c3 100644
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/HelperFunctionNames.cs
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/HelperFunctionNames.cs
@@ -2,6 +2,10 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
 {
     static class HelperFunctionNames
     {
+        public static string GetBindlessHandle = "Helper_GetBindlessHandle";
+        public static string GetBindlessTextureIndexVk = "Helper_GetBindlessTextureIndex";
+        public static string GetBindlessSamplerIndexVk = "Helper_GetBindlessSamplerIndex";
+
         public static string MultiplyHighS32 = "Helper_MultiplyHighS32";
         public static string MultiplyHighU32 = "Helper_MultiplyHighU32";
 
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenMemory.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenMemory.cs
index 2e90bd16d..9e59adb20 100644
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenMemory.cs
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenMemory.cs
@@ -16,33 +16,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
 
             bool isBindless = (texOp.Flags & TextureFlags.Bindless) != 0;
 
-            // TODO: Bindless texture support. For now we just return 0/do nothing.
-            if (isBindless)
-            {
-                switch (texOp.Inst)
-                {
-                    case Instruction.ImageStore:
-                        return "// imageStore(bindless)";
-                    case Instruction.ImageLoad:
-                        AggregateType componentType = texOp.Format.GetComponentType();
-
-                        NumberFormatter.TryFormat(0, componentType, out string imageConst);
-
-                        AggregateType outputType = texOp.GetVectorType(componentType);
-
-                        if ((outputType & AggregateType.ElementCountMask) != 0)
-                        {
-                            return $"{Declarations.GetVarTypeName(context, outputType, precise: false)}({imageConst})";
-                        }
-
-                        return imageConst;
-                    default:
-                        return NumberFormatter.FormatInt(0);
-                }
-            }
-
             bool isArray = (texOp.Type & SamplerType.Array) != 0;
-            bool isIndexed = (texOp.Type & SamplerType.Indexed) != 0;
 
             var texCallBuilder = new StringBuilder();
 
@@ -70,21 +44,27 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
                 texCallBuilder.Append(texOp.Inst == Instruction.ImageLoad ? "imageLoad" : "imageStore");
             }
 
-            int srcIndex = isBindless ? 1 : 0;
+            int srcIndex = 0;
 
             string Src(AggregateType type)
             {
                 return GetSoureExpr(context, texOp.GetSource(srcIndex++), type);
             }
 
-            string indexExpr = null;
+            AggregateType type = texOp.Format.GetComponentType();
 
-            if (isIndexed)
+            string bindlessHandle = null;
+            string imageName;
+
+            if (isBindless)
             {
-                indexExpr = Src(AggregateType.S32);
+                bindlessHandle = Src(AggregateType.S32);
+                imageName = GetBindlessImage(context, texOp.Type, type, bindlessHandle);
+            }
+            else
+            {
+                imageName = GetImageName(context.Properties, texOp);
             }
-
-            string imageName = GetImageName(context.Properties, texOp, indexExpr);
 
             texCallBuilder.Append('(');
             texCallBuilder.Append(imageName);
@@ -117,8 +97,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
 
             if (texOp.Inst == Instruction.ImageStore)
             {
-                AggregateType type = texOp.Format.GetComponentType();
-
                 string[] cElems = new string[4];
 
                 for (int index = 0; index < 4; index++)
@@ -150,8 +128,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
 
             if (texOp.Inst == Instruction.ImageAtomic)
             {
-                AggregateType type = texOp.Format.GetComponentType();
-
                 if ((texOp.Flags & TextureFlags.AtomicMask) == TextureFlags.CAS)
                 {
                     Append(Src(type)); // Compare value.
@@ -207,18 +183,9 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
                 return NumberFormatter.FormatFloat(0);
             }
 
-            bool isIndexed = (texOp.Type & SamplerType.Indexed) != 0;
+            string samplerName = GetSamplerName(context.Properties, texOp);
 
-            string indexExpr = null;
-
-            if (isIndexed)
-            {
-                indexExpr = GetSoureExpr(context, texOp.GetSource(0), AggregateType.S32);
-            }
-
-            string samplerName = GetSamplerName(context.Properties, texOp, indexExpr);
-
-            int coordsIndex = isBindless || isIndexed ? 1 : 0;
+            int coordsIndex = isBindless ? 1 : 0;
 
             string coordsExpr;
 
@@ -260,7 +227,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
             bool hasOffsets = (texOp.Flags & TextureFlags.Offsets) != 0;
 
             bool isArray = (texOp.Type & SamplerType.Array) != 0;
-            bool isIndexed = (texOp.Type & SamplerType.Indexed) != 0;
             bool isMultisample = (texOp.Type & SamplerType.Multisample) != 0;
             bool isShadow = (texOp.Type & SamplerType.Shadow) != 0;
 
@@ -286,24 +252,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
                 hasLodLevel = false;
             }
 
-            // TODO: Bindless texture support. For now we just return 0.
-            if (isBindless)
-            {
-                string scalarValue = NumberFormatter.FormatFloat(0);
-
-                if (colorIsVector)
-                {
-                    AggregateType outputType = texOp.GetVectorType(AggregateType.FP32);
-
-                    if ((outputType & AggregateType.ElementCountMask) != 0)
-                    {
-                        return $"{Declarations.GetVarTypeName(context, outputType, precise: false)}({scalarValue})";
-                    }
-                }
-
-                return scalarValue;
-            }
-
             string texCall = intCoords ? "texelFetch" : "texture";
 
             if (isGather)
@@ -328,23 +276,24 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
                 texCall += "Offsets";
             }
 
-            int srcIndex = isBindless ? 1 : 0;
+            int srcIndex = 0;
 
             string Src(AggregateType type)
             {
                 return GetSoureExpr(context, texOp.GetSource(srcIndex++), type);
             }
 
-            string indexExpr = null;
+            string bindlessHandle = null;
 
-            if (isIndexed)
+            if (isBindless)
             {
-                indexExpr = Src(AggregateType.S32);
+                bindlessHandle = Src(AggregateType.S32);
+                texCall += "(" + GetBindlessSampler(context, texOp.Type, bindlessHandle);
+            }
+            else
+            {
+                texCall += "(" + GetSamplerName(context.Properties, texOp);
             }
-
-            string samplerName = GetSamplerName(context.Properties, texOp, indexExpr);
-
-            texCall += "(" + samplerName;
 
             int coordsCount = texOp.Type.GetDimensions();
 
@@ -523,22 +472,11 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
 
             bool isBindless = (texOp.Flags & TextureFlags.Bindless) != 0;
 
-            // TODO: Bindless texture support. For now we just return 0.
-            if (isBindless)
-            {
-                return NumberFormatter.FormatInt(0);
-            }
+            string bindlessHandle = isBindless ? GetSoureExpr(context, operation.GetSource(0), AggregateType.S32) : null;
 
-            bool isIndexed = (texOp.Type & SamplerType.Indexed) != 0;
-
-            string indexExpr = null;
-
-            if (isIndexed)
-            {
-                indexExpr = GetSoureExpr(context, texOp.GetSource(0), AggregateType.S32);
-            }
-
-            string samplerName = GetSamplerName(context.Properties, texOp, indexExpr);
+            string samplerName = isBindless
+                ? GetBindlessSampler(context, texOp.Type, bindlessHandle)
+                : GetSamplerName(context.Properties, texOp);
 
             return $"textureSamples({samplerName})";
         }
@@ -549,22 +487,11 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
 
             bool isBindless = (texOp.Flags & TextureFlags.Bindless) != 0;
 
-            // TODO: Bindless texture support. For now we just return 0.
-            if (isBindless)
-            {
-                return NumberFormatter.FormatInt(0);
-            }
+            string bindlessHandle = isBindless ? GetSoureExpr(context, operation.GetSource(0), AggregateType.S32) : null;
 
-            bool isIndexed = (texOp.Type & SamplerType.Indexed) != 0;
-
-            string indexExpr = null;
-
-            if (isIndexed)
-            {
-                indexExpr = GetSoureExpr(context, texOp.GetSource(0), AggregateType.S32);
-            }
-
-            string samplerName = GetSamplerName(context.Properties, texOp, indexExpr);
+            string samplerName = isBindless
+                ? GetBindlessSampler(context, texOp.Type, bindlessHandle)
+                : GetSamplerName(context.Properties, texOp);
 
             if (texOp.Index == 3)
             {
@@ -572,13 +499,13 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
             }
             else
             {
-                context.Properties.Textures.TryGetValue(texOp.Binding, out TextureDefinition definition);
+                context.Properties.Textures.TryGetValue(SetBindingPair.Unpack(texOp.Binding), out TextureDefinition definition);
                 bool hasLod = !definition.Type.HasFlag(SamplerType.Multisample) && (definition.Type & SamplerType.Mask) != SamplerType.TextureBuffer;
                 string texCall;
 
                 if (hasLod)
                 {
-                    int lodSrcIndex = isBindless || isIndexed ? 1 : 0;
+                    int lodSrcIndex = isBindless ? 1 : 0;
                     IAstNode lod = operation.GetSource(lodSrcIndex);
                     string lodExpr = GetSoureExpr(context, lod, GetSrcVarType(operation.Inst, lodSrcIndex));
 
@@ -619,8 +546,8 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
 
                     int binding = bindingIndex.Value;
                     BufferDefinition buffer = storageKind == StorageKind.ConstantBuffer
-                        ? context.Properties.ConstantBuffers[binding]
-                        : context.Properties.StorageBuffers[binding];
+                        ? context.Properties.ConstantBuffers[SetBindingPair.Unpack(binding)]
+                        : context.Properties.StorageBuffers[SetBindingPair.Unpack(binding)];
 
                     if (operation.GetSource(srcIndex++) is not AstOperand fieldIndex || fieldIndex.Type != OperandType.Constant)
                     {
@@ -748,28 +675,52 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
             return varName;
         }
 
-        private static string GetSamplerName(ShaderProperties resourceDefinitions, AstTextureOperation texOp, string indexExpr)
+        private static string GetBindlessSampler(CodeGenContext context, SamplerType type, string bindlessHandle)
         {
-            string name = resourceDefinitions.Textures[texOp.Binding].Name;
+            string samplerType = type.ToGlslSamplerType();
 
-            if (texOp.Type.HasFlag(SamplerType.Indexed))
+            if (context.TargetApi == TargetApi.Vulkan)
             {
-                name = $"{name}[{indexExpr}]";
-            }
+                string textureIndex = $"{HelperFunctionNames.GetBindlessTextureIndexVk}({bindlessHandle})";
+                string samplerIndex = $"{HelperFunctionNames.GetBindlessSamplerIndexVk}({bindlessHandle})";
 
-            return name;
+                string bindlessTextureArrayName = context.OperandManager.BindlessTextures[type & ~SamplerType.Shadow];
+                string bindlessSamplerArrayName = context.OperandManager.BindlessTextures[SamplerType.None];
+
+                return $"{samplerType}({bindlessTextureArrayName}[{textureIndex}], {bindlessSamplerArrayName}[{samplerIndex}])";
+            }
+            else
+            {
+                return $"{samplerType}({HelperFunctionNames.GetBindlessHandle}({bindlessHandle}))";
+            }
         }
 
-        private static string GetImageName(ShaderProperties resourceDefinitions, AstTextureOperation texOp, string indexExpr)
+        private static string GetBindlessImage(CodeGenContext context, SamplerType type, AggregateType componentType, string bindlessHandle)
         {
-            string name = resourceDefinitions.Images[texOp.Binding].Name;
+            string imageType = type.ToGlslImageType(componentType);
 
-            if (texOp.Type.HasFlag(SamplerType.Indexed))
+            if (context.TargetApi == TargetApi.Vulkan)
             {
-                name = $"{name}[{indexExpr}]";
-            }
+                string textureIndex = $"{HelperFunctionNames.GetBindlessTextureIndexVk}({bindlessHandle})";
 
-            return name;
+                string bindlessImageArrayName = context.OperandManager.BindlessImages[type];
+
+                return $"{bindlessImageArrayName}[{textureIndex}]";
+            }
+            else
+            {
+                return $"{imageType}({HelperFunctionNames.GetBindlessHandle}({bindlessHandle}))";
+            }
+        }
+
+        private static string GetSamplerName(ShaderProperties resourceDefinitions, AstTextureOperation texOp)
+        {
+            return resourceDefinitions.Textures[SetBindingPair.Unpack(texOp.Binding)].Name;
+        }
+
+        private static string GetImageName(ShaderProperties resourceDefinitions, AstTextureOperation texOp)
+        {
+            return resourceDefinitions.Images[SetBindingPair.Unpack(texOp.Binding)].Name;
         }
 
         private static string GetMask(int index)
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/OperandManager.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/OperandManager.cs
index 53ecc4531..8d1ee906b 100644
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/OperandManager.cs
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/OperandManager.cs
@@ -13,9 +13,15 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
     {
         private readonly Dictionary<AstOperand, string> _locals;
 
+        public Dictionary<SamplerType, string> BindlessTextures { get; }
+        public Dictionary<SamplerType, string> BindlessImages { get; }
+
         public OperandManager()
         {
-            _locals = new Dictionary<AstOperand, string>();
+            _locals = new();
+
+            BindlessTextures = new();
+            BindlessImages = new();
         }
 
         public string DeclareLocal(AstOperand operand)
@@ -68,8 +74,8 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
                             }
 
                             BufferDefinition buffer = operation.StorageKind == StorageKind.ConstantBuffer
-                                ? context.Properties.ConstantBuffers[bindingIndex.Value]
-                                : context.Properties.StorageBuffers[bindingIndex.Value];
+                                ? context.Properties.ConstantBuffers[SetBindingPair.Unpack(bindingIndex.Value)]
+                                : context.Properties.StorageBuffers[SetBindingPair.Unpack(bindingIndex.Value)];
                             StructureField field = buffer.Type.Fields[fieldIndex.Value];
 
                             return field.Type & AggregateType.ElementTypeMask;
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/CodeGenContext.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/CodeGenContext.cs
index 53267c60b..8ff6a6dec 100644
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/CodeGenContext.cs
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/CodeGenContext.cs
@@ -29,15 +29,14 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
 
         public Dictionary<int, Instruction> ConstantBuffers { get; } = new();
         public Dictionary<int, Instruction> StorageBuffers { get; } = new();
-
         public Dictionary<int, Instruction> LocalMemories { get; } = new();
         public Dictionary<int, Instruction> SharedMemories { get; } = new();
-
         public Dictionary<int, SamplerType> SamplersTypes { get; } = new();
         public Dictionary<int, (Instruction, Instruction, Instruction)> Samplers { get; } = new();
         public Dictionary<int, (Instruction, Instruction)> Images { get; } = new();
-
         public Dictionary<IoDefinition, Instruction> Inputs { get; } = new();
+        public Dictionary<SamplerType, (Instruction, Instruction, Instruction, Instruction)> BindlessTextures { get; } = new();
+        public Dictionary<SamplerType, (Instruction, Instruction, Instruction)> BindlessImages { get; } = new();
         public Dictionary<IoDefinition, Instruction> Outputs { get; } = new();
         public Dictionary<IoDefinition, Instruction> InputsPerPatch { get; } = new();
         public Dictionary<IoDefinition, Instruction> OutputsPerPatch { get; } = new();
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Declarations.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Declarations.cs
index 45933a21b..be95e4531 100644
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Declarations.cs
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Declarations.cs
@@ -43,12 +43,12 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
 
         public static void DeclareAll(CodeGenContext context, StructuredProgramInfo info)
         {
-            DeclareConstantBuffers(context, context.Properties.ConstantBuffers.Values);
-            DeclareStorageBuffers(context, context.Properties.StorageBuffers.Values);
+            DeclareConstantBuffers(context, context.Properties.ConstantBuffers);
+            DeclareStorageBuffers(context, context.Properties.StorageBuffers);
             DeclareMemories(context, context.Properties.LocalMemories, context.LocalMemories, StorageClass.Private);
             DeclareMemories(context, context.Properties.SharedMemories, context.SharedMemories, StorageClass.Workgroup);
-            DeclareSamplers(context, context.Properties.Textures.Values);
-            DeclareImages(context, context.Properties.Images.Values);
+            DeclareSamplers(context, context.Properties.Textures);
+            DeclareImages(context, context.Properties.Images);
             DeclareInputsAndOutputs(context, info);
         }
 
@@ -58,7 +58,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
             Dictionary<int, SpvInstruction> dict,
             StorageClass storage)
         {
-            foreach ((int id, MemoryDefinition memory) in memories)
+            foreach ((int id, var memory) in memories)
             {
                 var pointerType = context.TypePointer(storage, context.GetType(memory.Type, memory.ArrayLength));
                 var variable = context.Variable(pointerType, storage);
@@ -69,21 +69,21 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
             }
         }
 
-        private static void DeclareConstantBuffers(CodeGenContext context, IEnumerable<BufferDefinition> buffers)
+        private static void DeclareConstantBuffers(CodeGenContext context, IReadOnlyDictionary<SetBindingPair, BufferDefinition> buffers)
         {
             DeclareBuffers(context, buffers, isBuffer: false);
         }
 
-        private static void DeclareStorageBuffers(CodeGenContext context, IEnumerable<BufferDefinition> buffers)
+        private static void DeclareStorageBuffers(CodeGenContext context, IReadOnlyDictionary<SetBindingPair, BufferDefinition> buffers)
         {
             DeclareBuffers(context, buffers, isBuffer: true);
         }
 
-        private static void DeclareBuffers(CodeGenContext context, IEnumerable<BufferDefinition> buffers, bool isBuffer)
+        private static void DeclareBuffers(CodeGenContext context, IReadOnlyDictionary<SetBindingPair, BufferDefinition> buffers, bool isBuffer)
         {
             HashSet<SpvInstruction> decoratedTypes = new();
 
-            foreach (BufferDefinition buffer in buffers)
+            foreach ((SetBindingPair sbPair, var buffer) in buffers)
             {
                 int setIndex = context.TargetApi == TargetApi.Vulkan ? buffer.Set : 0;
                 int alignment = buffer.Layout == BufferLayout.Std140 ? 16 : 4;
@@ -145,46 +145,71 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
 
                 if (isBuffer)
                 {
-                    context.StorageBuffers.Add(buffer.Binding, variable);
+                    context.StorageBuffers.Add(sbPair.Pack(), variable);
                 }
                 else
                 {
-                    context.ConstantBuffers.Add(buffer.Binding, variable);
+                    context.ConstantBuffers.Add(sbPair.Pack(), variable);
                 }
             }
         }
 
-        private static void DeclareSamplers(CodeGenContext context, IEnumerable<TextureDefinition> samplers)
+        private static void DeclareSamplers(CodeGenContext context, IReadOnlyDictionary<SetBindingPair, TextureDefinition> samplers)
         {
-            foreach (var sampler in samplers)
+            foreach ((SetBindingPair sbPair, var sampler) in samplers)
             {
                 int setIndex = context.TargetApi == TargetApi.Vulkan ? sampler.Set : 0;
 
-                var dim = (sampler.Type & SamplerType.Mask) switch
+                SpvInstruction imageType;
+                SpvInstruction sampledImageType;
+
+                if (sampler.Type != SamplerType.None)
                 {
-                    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 \"{sampler.Type & SamplerType.Mask}\"."),
-                };
+                    var dim = GetDim(sampler.Type);
 
-                var imageType = context.TypeImage(
-                    context.TypeFP32(),
-                    dim,
-                    sampler.Type.HasFlag(SamplerType.Shadow),
-                    sampler.Type.HasFlag(SamplerType.Array),
-                    sampler.Type.HasFlag(SamplerType.Multisample),
-                    1,
-                    ImageFormat.Unknown);
+                    imageType = context.TypeImage(
+                        context.TypeFP32(),
+                        dim,
+                        sampler.Type.HasFlag(SamplerType.Shadow),
+                        sampler.Type.HasFlag(SamplerType.Array),
+                        sampler.Type.HasFlag(SamplerType.Multisample),
+                        1,
+                        ImageFormat.Unknown);
 
-                var sampledImageType = context.TypeSampledImage(imageType);
-                var sampledImagePointerType = context.TypePointer(StorageClass.UniformConstant, sampledImageType);
-                var sampledImageVariable = context.Variable(sampledImagePointerType, StorageClass.UniformConstant);
+                    sampledImageType = context.TypeSampledImage(imageType);
+                }
+                else
+                {
+                    imageType = sampledImageType = context.TypeSampler();
+                }
 
-                context.Samplers.Add(sampler.Binding, (imageType, sampledImageType, sampledImageVariable));
-                context.SamplersTypes.Add(sampler.Binding, sampler.Type);
+                var imageTypeForSampler = sampler.Type.HasFlag(SamplerType.Separate) ? imageType : sampledImageType;
+                var sampledImagePointerType = context.TypePointer(StorageClass.UniformConstant, imageTypeForSampler);
+
+                SpvInstruction sampledImageArrayPointerType = sampledImagePointerType;
+
+                if (sampler.ArraySize == 0)
+                {
+                    var sampledImageArrayType = context.TypeRuntimeArray(imageTypeForSampler);
+                    sampledImageArrayPointerType = context.TypePointer(StorageClass.UniformConstant, sampledImageArrayType);
+                }
+                else if (sampler.ArraySize != 1)
+                {
+                    var sampledImageArrayType = context.TypeArray(imageTypeForSampler, context.Constant(context.TypeU32(), sampler.ArraySize));
+                    sampledImageArrayPointerType = context.TypePointer(StorageClass.UniformConstant, sampledImageArrayType);
+                }
+
+                var sampledImageVariable = context.Variable(sampledImageArrayPointerType, StorageClass.UniformConstant);
+
+                if (sampler.ArraySize != 1)
+                {
+                    context.BindlessTextures[sampler.Type & ~(SamplerType.Shadow | SamplerType.Separate)] = (imageType, sampledImageType, sampledImagePointerType, sampledImageVariable);
+                }
+                else
+                {
+                    context.Samplers.Add(sbPair.Pack(), (imageType, sampledImageType, sampledImageVariable));
+                    context.SamplersTypes.Add(sbPair.Pack(), sampler.Type);
+                }
 
                 context.Name(sampledImageVariable, sampler.Name);
                 context.Decorate(sampledImageVariable, Decoration.DescriptorSet, (LiteralInteger)setIndex);
@@ -193,9 +218,9 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
             }
         }
 
-        private static void DeclareImages(CodeGenContext context, IEnumerable<TextureDefinition> images)
+        private static void DeclareImages(CodeGenContext context, IReadOnlyDictionary<SetBindingPair, TextureDefinition> images)
         {
-            foreach (var image in images)
+            foreach ((SetBindingPair sbPair, var image) in images)
             {
                 int setIndex = context.TargetApi == TargetApi.Vulkan ? image.Set : 0;
 
@@ -211,9 +236,30 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
                     GetImageFormat(image.Format));
 
                 var imagePointerType = context.TypePointer(StorageClass.UniformConstant, imageType);
-                var imageVariable = context.Variable(imagePointerType, StorageClass.UniformConstant);
 
-                context.Images.Add(image.Binding, (imageType, imageVariable));
+                SpvInstruction imageArrayPointerType = imagePointerType;
+
+                if (image.ArraySize == 0)
+                {
+                    var imageArrayType = context.TypeRuntimeArray(imageType);
+                    imageArrayPointerType = context.TypePointer(StorageClass.UniformConstant, imageArrayType);
+                }
+                else if (image.ArraySize != 1)
+                {
+                    var imageArrayType = context.TypeArray(imageType, context.Constant(context.TypeU32(), image.ArraySize));
+                    imageArrayPointerType = context.TypePointer(StorageClass.UniformConstant, imageArrayType);
+                }
+
+                var imageVariable = context.Variable(imageArrayPointerType, StorageClass.UniformConstant);
+
+                if (image.ArraySize != 1)
+                {
+                    context.BindlessImages[image.Type] = (imageType, imagePointerType, imageVariable);
+                }
+                else
+                {
+                    context.Images.Add(sbPair.Pack(), (imageType, imageVariable));
+                }
 
                 context.Name(imageVariable, image.Name);
                 context.Decorate(imageVariable, Decoration.DescriptorSet, (LiteralInteger)setIndex);
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs
index 50a73ab83..a6f49c692 100644
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs
@@ -595,31 +595,16 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
 
             var componentType = texOp.Format.GetComponentType();
 
-            // TODO: Bindless texture support. For now we just return 0/do nothing.
-            if (isBindless)
-            {
-                return new OperationResult(componentType, componentType switch
-                {
-                    AggregateType.S32 => context.Constant(context.TypeS32(), 0),
-                    AggregateType.U32 => context.Constant(context.TypeU32(), 0u),
-                    _ => context.Constant(context.TypeFP32(), 0f),
-                });
-            }
-
             bool isArray = (texOp.Type & SamplerType.Array) != 0;
-            bool isIndexed = (texOp.Type & SamplerType.Indexed) != 0;
 
-            int srcIndex = isBindless ? 1 : 0;
+            int srcIndex = 0;
 
             SpvInstruction Src(AggregateType type)
             {
                 return context.Get(type, texOp.GetSource(srcIndex++));
             }
 
-            if (isIndexed)
-            {
-                Src(AggregateType.S32);
-            }
+            SpvInstruction bindlessHandle = isBindless ? Src(AggregateType.S32) : null;
 
             int coordsCount = texOp.Type.GetDimensions();
 
@@ -646,9 +631,19 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
 
             SpvInstruction value = Src(componentType);
 
-            (var imageType, var imageVariable) = context.Images[texOp.Binding];
+            SpvInstruction imageVariable;
 
-            context.Load(imageType, imageVariable);
+            if (isBindless)
+            {
+                (_, var bindlessImagePointerType, var bindlessImageVariable) = context.BindlessImages[texOp.Type];
+
+                var imageIndex = GenerateBindlessTextureHandleToIndex(context, bindlessHandle);
+                imageVariable = context.AccessChain(bindlessImagePointerType, bindlessImageVariable, imageIndex);
+            }
+            else
+            {
+                (_, imageVariable) = context.Images[texOp.Binding];
+            }
 
             SpvInstruction resultType = context.GetType(componentType);
             SpvInstruction imagePointerType = context.TypePointer(StorageClass.Image, resultType);
@@ -687,26 +682,16 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
 
             var componentType = texOp.Format.GetComponentType();
 
-            // TODO: Bindless texture support. For now we just return 0/do nothing.
-            if (isBindless)
-            {
-                return GetZeroOperationResult(context, texOp, componentType, isVector: true);
-            }
-
             bool isArray = (texOp.Type & SamplerType.Array) != 0;
-            bool isIndexed = (texOp.Type & SamplerType.Indexed) != 0;
 
-            int srcIndex = isBindless ? 1 : 0;
+            int srcIndex = 0;
 
             SpvInstruction Src(AggregateType type)
             {
                 return context.Get(type, texOp.GetSource(srcIndex++));
             }
 
-            if (isIndexed)
-            {
-                Src(AggregateType.S32);
-            }
+            SpvInstruction bindlessHandle = isBindless ? Src(AggregateType.S32) : null;
 
             int coordsCount = texOp.Type.GetDimensions();
 
@@ -731,9 +716,27 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
                 pCoords = Src(AggregateType.S32);
             }
 
-            (var imageType, var imageVariable) = context.Images[texOp.Binding];
+            SpvInstruction bindlessIndex;
+            SpvInstruction image;
+
+            if (isBindless)
+            {
+                (var imageType, var imagePointerType, var imageVariable) = context.BindlessImages[texOp.Type];
+
+                var imageIndex = GenerateBindlessTextureHandleToIndex(context, bindlessHandle);
+                var imagePointer = context.AccessChain(imagePointerType, imageVariable, imageIndex);
+
+                bindlessIndex = imageIndex;
+                image = context.Load(imageType, imagePointer);
+            }
+            else
+            {
+                (var imageType, var imageVariable) = context.Images[texOp.Binding];
+
+                bindlessIndex = null;
+                image = context.Load(imageType, imageVariable);
+            }
 
-            var image = context.Load(imageType, imageVariable);
             var imageComponentType = context.GetType(componentType);
             var swizzledResultType = texOp.GetVectorType(componentType);
 
@@ -749,26 +752,16 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
 
             bool isBindless = (texOp.Flags & TextureFlags.Bindless) != 0;
 
-            // TODO: Bindless texture support. For now we just return 0/do nothing.
-            if (isBindless)
-            {
-                return OperationResult.Invalid;
-            }
-
             bool isArray = (texOp.Type & SamplerType.Array) != 0;
-            bool isIndexed = (texOp.Type & SamplerType.Indexed) != 0;
 
-            int srcIndex = isBindless ? 1 : 0;
+            int srcIndex = 0;
 
             SpvInstruction Src(AggregateType type)
             {
                 return context.Get(type, texOp.GetSource(srcIndex++));
             }
 
-            if (isIndexed)
-            {
-                Src(AggregateType.S32);
-            }
+            SpvInstruction bindlessHandle = isBindless ? Src(AggregateType.S32) : null;
 
             int coordsCount = texOp.Type.GetDimensions();
 
@@ -818,9 +811,23 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
 
             var texel = context.CompositeConstruct(context.TypeVector(context.GetType(componentType), ComponentsCount), cElems);
 
-            (var imageType, var imageVariable) = context.Images[texOp.Binding];
+            SpvInstruction image;
 
-            var image = context.Load(imageType, imageVariable);
+            if (isBindless)
+            {
+                (var imageType, var imagePointerType, var imageVariable) = context.BindlessImages[texOp.Type];
+
+                var imageIndex = GenerateBindlessTextureHandleToIndex(context, bindlessHandle);
+                var imagePointer = context.AccessChain(imagePointerType, imageVariable, imageIndex);
+
+                image = context.Load(imageType, imagePointer);
+            }
+            else
+            {
+                (var imageType, var imageVariable) = context.Images[texOp.Binding];
+
+                image = context.Load(imageType, imageVariable);
+            }
 
             context.ImageWrite(image, pCoords, texel, ImageOperandsMask.MaskNone);
 
@@ -856,14 +863,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
 
             bool isBindless = (texOp.Flags & TextureFlags.Bindless) != 0;
 
-            bool isIndexed = (texOp.Type & SamplerType.Indexed) != 0;
-
-            // TODO: Bindless texture support. For now we just return 0.
-            if (isBindless)
-            {
-                return new OperationResult(AggregateType.S32, context.Constant(context.TypeS32(), 0));
-            }
-
             int srcIndex = 0;
 
             SpvInstruction Src(AggregateType type)
@@ -871,10 +870,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
                 return context.Get(type, texOp.GetSource(srcIndex++));
             }
 
-            if (isIndexed)
-            {
-                Src(AggregateType.S32);
-            }
+            SpvInstruction bindlessHandle = isBindless ? Src(AggregateType.S32) : null;
 
             int pCount = texOp.Type.GetDimensions();
 
@@ -897,9 +893,23 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
                 pCoords = Src(AggregateType.FP32);
             }
 
-            (_, var sampledImageType, var sampledImageVariable) = context.Samplers[texOp.Binding];
+            SpvInstruction image;
 
-            var image = context.Load(sampledImageType, sampledImageVariable);
+            if (isBindless)
+            {
+                (var imageType, _, var imagePointerType, var imageVariable) = context.BindlessTextures[texOp.Type & ~SamplerType.Shadow];
+
+                var imageIndex = GenerateBindlessTextureHandleToIndex(context, bindlessHandle);
+                var imagePointer = context.AccessChain(imagePointerType, imageVariable, imageIndex);
+
+                image = context.Load(imageType, imagePointer);
+            }
+            else
+            {
+                (_, var sampledImageType, var sampledImageVariable) = context.Samplers[texOp.Binding];
+
+                image = context.Load(sampledImageType, sampledImageVariable);
+            }
 
             var resultType = context.TypeVector(context.TypeFP32(), 2);
             var packed = context.ImageQueryLod(resultType, image, pCoords);
@@ -1192,29 +1202,19 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
             bool hasOffsets = (texOp.Flags & TextureFlags.Offsets) != 0;
 
             bool isArray = (texOp.Type & SamplerType.Array) != 0;
-            bool isIndexed = (texOp.Type & SamplerType.Indexed) != 0;
             bool isMultisample = (texOp.Type & SamplerType.Multisample) != 0;
             bool isShadow = (texOp.Type & SamplerType.Shadow) != 0;
 
             bool colorIsVector = isGather || !isShadow;
 
-            // TODO: Bindless texture support. For now we just return 0.
-            if (isBindless)
-            {
-                return GetZeroOperationResult(context, texOp, AggregateType.FP32, colorIsVector);
-            }
-
-            int srcIndex = isBindless ? 1 : 0;
+            int srcIndex = 0;
 
             SpvInstruction Src(AggregateType type)
             {
                 return context.Get(type, texOp.GetSource(srcIndex++));
             }
 
-            if (isIndexed)
-            {
-                Src(AggregateType.S32);
-            }
+            SpvInstruction bindlessHandle = isBindless ? Src(AggregateType.S32) : null;
 
             int coordsCount = texOp.Type.GetDimensions();
 
@@ -1262,6 +1262,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
             }
 
             SpvInstruction pCoords = AssemblePVector(pCount);
+            SpvInstruction bindlessIndex = isBindless ? GenerateBindlessTextureHandleToIndex(context, bindlessHandle) : null;
 
             SpvInstruction AssembleDerivativesVector(int count)
             {
@@ -1421,9 +1422,33 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
 
             var resultType = colorIsVector ? context.TypeVector(context.TypeFP32(), 4) : context.TypeFP32();
 
-            (var imageType, var sampledImageType, var sampledImageVariable) = context.Samplers[texOp.Binding];
+            SpvInstruction imageType;
+            SpvInstruction image;
 
-            var image = context.Load(sampledImageType, sampledImageVariable);
+            if (isBindless)
+            {
+                (imageType, var sampledImageType, var imagePointerType, var imageVariable) = context.BindlessTextures[texOp.Type & ~SamplerType.Shadow];
+
+                var imageIndex = GenerateBindlessTextureHandleToIndex(context, bindlessHandle);
+                var imagePointer = context.AccessChain(imagePointerType, imageVariable, imageIndex);
+
+                image = context.Load(imageType, imagePointer);
+
+                (_, var samplerType, var samplerPointerType, var bindlessSamplerArray) = context.BindlessTextures[SamplerType.None];
+
+                var samplerIndex = GenerateBindlessSamplerHandleToIndex(context, bindlessHandle);
+                var samplerPointer = context.AccessChain(samplerPointerType, bindlessSamplerArray, samplerIndex);
+
+                var sampler = context.Load(samplerType, samplerPointer);
+
+                image = context.SampledImage(sampledImageType, image, sampler);
+            }
+            else
+            {
+                (imageType, var sampledImageType, var sampledImageVariable) = context.Samplers[texOp.Binding];
+
+                image = context.Load(sampledImageType, sampledImageVariable);
+            }
 
             if (intCoords)
             {
@@ -1493,13 +1518,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
                 return new OperationResult(AggregateType.S32, context.Constant(context.TypeS32(), 0));
             }
 
-            bool isIndexed = (texOp.Type & SamplerType.Indexed) != 0;
-
-            if (isIndexed)
-            {
-                context.GetS32(texOp.GetSource(0));
-            }
-
             (var imageType, var sampledImageType, var sampledImageVariable) = context.Samplers[texOp.Binding];
 
             var image = context.Load(sampledImageType, sampledImageVariable);
@@ -1516,22 +1534,30 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
 
             bool isBindless = (texOp.Flags & TextureFlags.Bindless) != 0;
 
-            // TODO: Bindless texture support. For now we just return 0.
+            SpvInstruction bindlessIndex;
+            SpvInstruction imageType;
+            SpvInstruction image;
+
             if (isBindless)
             {
-                return new OperationResult(AggregateType.S32, context.Constant(context.TypeS32(), 0));
+                SpvInstruction bindlessHandle = context.GetS32(operation.GetSource(0));
+
+                (imageType, _, var imagePointerType, var imageVariable) = context.BindlessTextures[texOp.Type & ~SamplerType.Shadow];
+
+                var imageIndex = GenerateBindlessTextureHandleToIndex(context, bindlessHandle);
+                var imagePointer = context.AccessChain(imagePointerType, imageVariable, imageIndex);
+
+                bindlessIndex = imageIndex;
+                image = context.Load(imageType, imagePointer);
             }
-
-            bool isIndexed = (texOp.Type & SamplerType.Indexed) != 0;
-
-            if (isIndexed)
+            else
             {
-                context.GetS32(texOp.GetSource(0));
+                (imageType, var sampledImageType, var sampledImageVariable) = context.Samplers[texOp.Binding];
+
+                bindlessIndex = null;
+                image = context.Load(sampledImageType, sampledImageVariable);
             }
 
-            (var imageType, var sampledImageType, var sampledImageVariable) = context.Samplers[texOp.Binding];
-
-            var image = context.Load(sampledImageType, sampledImageVariable);
             image = context.Image(imageType, image);
 
             if (texOp.Index == 3)
@@ -1540,7 +1566,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
             }
             else
             {
-                var type = context.SamplersTypes[texOp.Binding];
+                var type = isBindless ? texOp.Type : context.SamplersTypes[texOp.Binding];
                 bool hasLod = !type.HasFlag(SamplerType.Multisample) && type != SamplerType.TextureBuffer;
 
                 int dimensions = (type & SamplerType.Mask) == SamplerType.TextureCube ? 2 : type.GetDimensions();
@@ -1556,7 +1582,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
 
                 if (hasLod)
                 {
-                    int lodSrcIndex = isBindless || isIndexed ? 1 : 0;
+                    int lodSrcIndex = isBindless ? 1 : 0;
                     var lod = context.GetS32(operation.GetSource(lodSrcIndex));
                     result = context.ImageQuerySizeLod(resultType, image, lod);
                 }
@@ -1638,6 +1664,51 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
             return new OperationResult(AggregateType.Bool, result);
         }
 
+        private static SpvInstruction GenerateBindlessTextureHandleToIndex(CodeGenContext context, SpvInstruction bindlessHandle)
+        {
+            var bindlessTable = context.ConstantBuffers[SetBindingPair.Pack(Constants.BindlessTextureSetIndex, Constants.BindlessTableBinding)];
+            var id = context.BitwiseAnd(context.TypeS32(), bindlessHandle, context.Constant(context.TypeS32(), 0xfffff));
+            var tableIndex = context.ShiftRightArithmetic(context.TypeS32(), id, context.Constant(context.TypeS32(), 8));
+
+            var pointerUint = context.TypePointer(StorageClass.Uniform, context.TypeU32());
+            var baseIndex = context.AccessChain(
+                pointerUint,
+                bindlessTable,
+                context.Constant(context.TypeS32(), 0),
+                tableIndex,
+                context.Constant(context.TypeU32(), 0));
+
+            baseIndex = context.Load(context.TypeU32(), baseIndex);
+
+            var idLow = context.BitwiseAnd(context.TypeS32(), id, context.Constant(context.TypeS32(), 0xff));
+            var index = context.BitwiseOr(context.TypeU32(), baseIndex, idLow);
+
+            return index;
+        }
+
+        private static SpvInstruction GenerateBindlessSamplerHandleToIndex(CodeGenContext context, SpvInstruction bindlessHandle)
+        {
+            var bindlessTable = context.ConstantBuffers[SetBindingPair.Pack(Constants.BindlessTextureSetIndex, Constants.BindlessTableBinding)];
+            var idHigh = context.ShiftRightArithmetic(context.TypeS32(), bindlessHandle, context.Constant(context.TypeS32(), 20));
+            var id = context.BitwiseAnd(context.TypeS32(), idHigh, context.Constant(context.TypeS32(), 0xfff));
+            var tableIndex = context.ShiftRightArithmetic(context.TypeS32(), id, context.Constant(context.TypeS32(), 8));
+
+            var pointerUint = context.TypePointer(StorageClass.Uniform, context.TypeU32());
+            var baseIndex = context.AccessChain(
+                pointerUint,
+                bindlessTable,
+                context.Constant(context.TypeS32(), 0),
+                tableIndex,
+                context.Constant(context.TypeU32(), 1));
+
+            baseIndex = context.Load(context.TypeU32(), baseIndex);
+
+            var idLow = context.BitwiseAnd(context.TypeS32(), id, context.Constant(context.TypeS32(), 0xff));
+            var index = context.BitwiseOr(context.TypeU32(), baseIndex, idLow);
+
+            return index;
+        }
+
         private static OperationResult GenerateCompare(
             CodeGenContext context,
             AstOperation operation,
@@ -1746,8 +1817,8 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
                     }
 
                     BufferDefinition buffer = storageKind == StorageKind.ConstantBuffer
-                        ? context.Properties.ConstantBuffers[bindingIndex.Value]
-                        : context.Properties.StorageBuffers[bindingIndex.Value];
+                        ? context.Properties.ConstantBuffers[SetBindingPair.Unpack(bindingIndex.Value)]
+                        : context.Properties.StorageBuffers[SetBindingPair.Unpack(bindingIndex.Value)];
                     StructureField field = buffer.Type.Fields[fieldIndex.Value];
 
                     storageClass = StorageClass.Uniform;
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/SpirvGenerator.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/SpirvGenerator.cs
index a1e9054f6..4f739c08f 100644
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/SpirvGenerator.cs
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/SpirvGenerator.cs
@@ -105,6 +105,17 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
                 context.AddCapability(Capability.ShaderViewportMaskNV);
             }
 
+            if (parameters.BindlessTextureFlags != BindlessTextureFlags.None)
+            {
+                context.AddExtension("SPV_EXT_descriptor_indexing");
+                context.AddCapability(Capability.Sampled1D);
+                context.AddCapability(Capability.Image1D);
+                context.AddCapability(Capability.SampledCubeArray);
+                context.AddCapability(Capability.ImageCubeArray);
+                context.AddCapability(Capability.StorageImageMultisample);
+                context.AddCapability(Capability.RuntimeDescriptorArray);
+            }
+
             if ((info.HelperFunctionsMask & NeedsInvocationIdMask) != 0)
             {
                 info.IoDefinitions.Add(new IoDefinition(StorageKind.Input, IoVariable.SubgroupLaneId));
diff --git a/src/Ryujinx.Graphics.Shader/Constants.cs b/src/Ryujinx.Graphics.Shader/Constants.cs
index 6317369f0..34cf8295f 100644
--- a/src/Ryujinx.Graphics.Shader/Constants.cs
+++ b/src/Ryujinx.Graphics.Shader/Constants.cs
@@ -10,5 +10,16 @@ namespace Ryujinx.Graphics.Shader
         public const int NvnBaseVertexByteOffset = 0x640;
         public const int NvnBaseInstanceByteOffset = 0x644;
         public const int NvnDrawIndexByteOffset = 0x648;
+
+        public const int VkConstantBufferSetIndex = 0;
+        public const int VkStorageBufferSetIndex = 1;
+        public const int VkTextureSetIndex = 2;
+        public const int VkImageSetIndex = 3;
+
+        // Bindless emulation.
+
+        public const int BindlessTextureSetIndex = 4;
+        public const int BindlessTableBinding = 0;
+        public const int BindlessScalesBinding = 1;
     }
 }
diff --git a/src/Ryujinx.Graphics.Shader/IGpuAccessor.cs b/src/Ryujinx.Graphics.Shader/IGpuAccessor.cs
index 29a5435e3..2df2b1442 100644
--- a/src/Ryujinx.Graphics.Shader/IGpuAccessor.cs
+++ b/src/Ryujinx.Graphics.Shader/IGpuAccessor.cs
@@ -404,6 +404,15 @@ namespace Ryujinx.Graphics.Shader
             return SamplerType.Texture2D;
         }
 
+        /// <summary>
+        /// Queries the number of the constant buffer where the texture handles are located.
+        /// </summary>
+        /// <returns>Constant buffer where the texture handles are located</returns>
+        int QueryTextureBufferIndex()
+        {
+            return 2; // NVN default.
+        }
+
         /// <summary>
         /// Queries texture coordinate normalization information.
         /// </summary>
diff --git a/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/Instruction.cs b/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/Instruction.cs
index e5695ebc2..4b1d173e3 100644
--- a/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/Instruction.cs
+++ b/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/Instruction.cs
@@ -161,5 +161,13 @@ namespace Ryujinx.Graphics.Shader.IntermediateRepresentation
             inst &= Instruction.Mask;
             return inst == Instruction.Lod || inst == Instruction.TextureQuerySamples || inst == Instruction.TextureQuerySize;
         }
+
+        public static bool IsImage(this Instruction inst)
+        {
+            inst &= Instruction.Mask;
+            return inst == Instruction.ImageLoad ||
+                   inst == Instruction.ImageStore ||
+                   inst == Instruction.ImageAtomic;
+        }
     }
 }
diff --git a/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/Operation.cs b/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/Operation.cs
index f5396a884..8eafecbd9 100644
--- a/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/Operation.cs
+++ b/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/Operation.cs
@@ -144,6 +144,26 @@ namespace Ryujinx.Graphics.Shader.IntermediateRepresentation
             }
         }
 
+        public void PrependSources(Operand[] operands)
+        {
+            int endIndex = operands.Length;
+
+            Array.Resize(ref _sources, endIndex + _sources.Length);
+            Array.Copy(_sources, 0, _sources, endIndex, _sources.Length - endIndex);
+
+            for (int index = 0; index < operands.Length; index++)
+            {
+                Operand source = operands[index];
+
+                if (source.Type == OperandType.LocalVariable)
+                {
+                    source.UseOps.Add(this);
+                }
+
+                _sources[index] = source;
+            }
+        }
+
         public void AppendSources(Operand[] operands)
         {
             int startIndex = _sources.Length;
diff --git a/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/TextureOperation.cs b/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/TextureOperation.cs
index fa5550a64..bf5be8cf6 100644
--- a/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/TextureOperation.cs
+++ b/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/TextureOperation.cs
@@ -26,19 +26,11 @@ namespace Ryujinx.Graphics.Shader.IntermediateRepresentation
             Binding = binding;
         }
 
-        public void TurnIntoIndexed(int binding)
-        {
-            Type |= SamplerType.Indexed;
-            Flags &= ~TextureFlags.Bindless;
-            Binding = binding;
-        }
-
         public void SetBinding(int binding)
         {
             if ((Flags & TextureFlags.Bindless) != 0)
             {
                 Flags &= ~TextureFlags.Bindless;
-
                 RemoveSource(0);
             }
 
@@ -49,5 +41,14 @@ namespace Ryujinx.Graphics.Shader.IntermediateRepresentation
         {
             Flags |= TextureFlags.LodLevel;
         }
+
+        public void TurnIntoBindless(Operand handle)
+        {
+            if ((Flags & TextureFlags.Bindless) == 0)
+            {
+                Flags |= TextureFlags.Bindless;
+                PrependSources(new Operand[] { handle });
+            }
+        }
     }
 }
diff --git a/src/Ryujinx.Graphics.Shader/ResourceReservationCounts.cs b/src/Ryujinx.Graphics.Shader/ResourceReservationCounts.cs
index c0bae8eab..a42ff8228 100644
--- a/src/Ryujinx.Graphics.Shader/ResourceReservationCounts.cs
+++ b/src/Ryujinx.Graphics.Shader/ResourceReservationCounts.cs
@@ -9,9 +9,9 @@ namespace Ryujinx.Graphics.Shader
         public readonly int ReservedTextures { get; }
         public readonly int ReservedImages { get; }
 
-        public ResourceReservationCounts(bool isTransformFeedbackEmulated, bool vertexAsCompute)
+        public ResourceReservationCounts(TargetApi targetApi, bool isTransformFeedbackEmulated, bool vertexAsCompute)
         {
-            ResourceReservations reservations = new(isTransformFeedbackEmulated, vertexAsCompute);
+            ResourceReservations reservations = new(targetApi, isTransformFeedbackEmulated, vertexAsCompute);
 
             ReservedConstantBuffers = reservations.ReservedConstantBuffers;
             ReservedStorageBuffers = reservations.ReservedStorageBuffers;
diff --git a/src/Ryujinx.Graphics.Shader/Ryujinx.Graphics.Shader.csproj b/src/Ryujinx.Graphics.Shader/Ryujinx.Graphics.Shader.csproj
index 8ccf5348f..23882013b 100644
--- a/src/Ryujinx.Graphics.Shader/Ryujinx.Graphics.Shader.csproj
+++ b/src/Ryujinx.Graphics.Shader/Ryujinx.Graphics.Shader.csproj
@@ -10,6 +10,8 @@
   </ItemGroup>
 
   <ItemGroup>
+    <EmbeddedResource Include="CodeGen\Glsl\HelperFunctions\GetBindlessHandle.glsl" />
+    <EmbeddedResource Include="CodeGen\Glsl\HelperFunctions\GetBindlessHandleVk.glsl" />
     <EmbeddedResource Include="CodeGen\Glsl\HelperFunctions\MultiplyHighS32.glsl" />
     <EmbeddedResource Include="CodeGen\Glsl\HelperFunctions\MultiplyHighU32.glsl" />
     <EmbeddedResource Include="CodeGen\Glsl\HelperFunctions\SwizzleAdd.glsl" />
diff --git a/src/Ryujinx.Graphics.Shader/SamplerType.cs b/src/Ryujinx.Graphics.Shader/SamplerType.cs
index 85e97368f..6794afd88 100644
--- a/src/Ryujinx.Graphics.Shader/SamplerType.cs
+++ b/src/Ryujinx.Graphics.Shader/SamplerType.cs
@@ -16,9 +16,9 @@ namespace Ryujinx.Graphics.Shader
         Mask = 0xff,
 
         Array = 1 << 8,
-        Indexed = 1 << 9,
-        Multisample = 1 << 10,
-        Shadow = 1 << 11,
+        Multisample = 1 << 9,
+        Shadow = 1 << 10,
+        Separate = 1 << 11,
     }
 
     static class SamplerTypeExtensions
@@ -40,6 +40,7 @@ namespace Ryujinx.Graphics.Shader
         {
             string typeName = (type & SamplerType.Mask) switch
             {
+                SamplerType.None => "sampler",
                 SamplerType.Texture1D => "sampler1D",
                 SamplerType.TextureBuffer => "samplerBuffer",
                 SamplerType.Texture2D => "sampler2D",
@@ -66,6 +67,31 @@ namespace Ryujinx.Graphics.Shader
             return typeName;
         }
 
+        public static string ToGlslTextureType(this SamplerType type)
+        {
+            string typeName = (type & SamplerType.Mask) switch
+            {
+                SamplerType.Texture1D => "texture1D",
+                SamplerType.TextureBuffer => "textureBuffer",
+                SamplerType.Texture2D => "texture2D",
+                SamplerType.Texture3D => "texture3D",
+                SamplerType.TextureCube => "textureCube",
+                _ => throw new ArgumentException($"Invalid texture type \"{type}\"."),
+            };
+
+            if ((type & SamplerType.Multisample) != 0)
+            {
+                typeName += "MS";
+            }
+
+            if ((type & SamplerType.Array) != 0)
+            {
+                typeName += "Array";
+            }
+
+            return typeName;
+        }
+
         public static string ToGlslImageType(this SamplerType type, AggregateType componentType)
         {
             string typeName = (type & SamplerType.Mask) switch
diff --git a/src/Ryujinx.Graphics.Shader/ShaderProgramInfo.cs b/src/Ryujinx.Graphics.Shader/ShaderProgramInfo.cs
index 22823ac38..38465032d 100644
--- a/src/Ryujinx.Graphics.Shader/ShaderProgramInfo.cs
+++ b/src/Ryujinx.Graphics.Shader/ShaderProgramInfo.cs
@@ -11,6 +11,7 @@ namespace Ryujinx.Graphics.Shader
         public ReadOnlyCollection<TextureDescriptor> Images { get; }
 
         public ShaderStage Stage { get; }
+        public BindlessTextureFlags BindlessTextureFlags { get; }
         public int GeometryVerticesPerPrimitive { get; }
         public int GeometryMaxOutputVertices { get; }
         public int ThreadsPerInputPrimitive { get; }
@@ -27,6 +28,7 @@ namespace Ryujinx.Graphics.Shader
             TextureDescriptor[] textures,
             TextureDescriptor[] images,
             ShaderStage stage,
+            BindlessTextureFlags bindlessTextureFlags,
             int geometryVerticesPerPrimitive,
             int geometryMaxOutputVertices,
             int threadsPerInputPrimitive,
@@ -43,6 +45,7 @@ namespace Ryujinx.Graphics.Shader
             Images = Array.AsReadOnly(images);
 
             Stage = stage;
+            BindlessTextureFlags = bindlessTextureFlags;
             GeometryVerticesPerPrimitive = geometryVerticesPerPrimitive;
             GeometryMaxOutputVertices = geometryMaxOutputVertices;
             ThreadsPerInputPrimitive = threadsPerInputPrimitive;
diff --git a/src/Ryujinx.Graphics.Shader/StructuredIr/SetBindingPair.cs b/src/Ryujinx.Graphics.Shader/StructuredIr/SetBindingPair.cs
new file mode 100644
index 000000000..c87109310
--- /dev/null
+++ b/src/Ryujinx.Graphics.Shader/StructuredIr/SetBindingPair.cs
@@ -0,0 +1,46 @@
+using System;
+
+namespace Ryujinx.Graphics.Shader
+{
+    readonly struct SetBindingPair : IEquatable<SetBindingPair>
+    {
+        public int Set { get; }
+        public int Binding { get; }
+
+        public SetBindingPair(int set, int binding)
+        {
+            Set = set;
+            Binding = binding;
+        }
+
+        public override bool Equals(object obj)
+        {
+            return base.Equals(obj);
+        }
+
+        public bool Equals(SetBindingPair other)
+        {
+            return other.Set == Set && other.Binding == Binding;
+        }
+
+        public override int GetHashCode()
+        {
+            return ((uint)Set | (ulong)(uint)Binding << 32).GetHashCode();
+        }
+
+        public int Pack()
+        {
+            return Pack(Set, Binding);
+        }
+
+        public static int Pack(int set, int binding)
+        {
+            return (ushort)set | (checked((ushort)binding) << 16);
+        }
+
+        public static SetBindingPair Unpack(int packed)
+        {
+            return new((ushort)packed, (ushort)((uint)packed >> 16));
+        }
+    }
+}
diff --git a/src/Ryujinx.Graphics.Shader/StructuredIr/ShaderProperties.cs b/src/Ryujinx.Graphics.Shader/StructuredIr/ShaderProperties.cs
index 8c12c2aaf..62013ac98 100644
--- a/src/Ryujinx.Graphics.Shader/StructuredIr/ShaderProperties.cs
+++ b/src/Ryujinx.Graphics.Shader/StructuredIr/ShaderProperties.cs
@@ -4,48 +4,48 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
 {
     class ShaderProperties
     {
-        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<SetBindingPair, BufferDefinition> _constantBuffers;
+        private readonly Dictionary<SetBindingPair, BufferDefinition> _storageBuffers;
+        private readonly Dictionary<SetBindingPair, TextureDefinition> _textures;
+        private readonly Dictionary<SetBindingPair, 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<SetBindingPair, BufferDefinition> ConstantBuffers => _constantBuffers;
+        public IReadOnlyDictionary<SetBindingPair, BufferDefinition> StorageBuffers => _storageBuffers;
+        public IReadOnlyDictionary<SetBindingPair, TextureDefinition> Textures => _textures;
+        public IReadOnlyDictionary<SetBindingPair, TextureDefinition> Images => _images;
         public IReadOnlyDictionary<int, MemoryDefinition> LocalMemories => _localMemories;
         public IReadOnlyDictionary<int, MemoryDefinition> SharedMemories => _sharedMemories;
 
         public ShaderProperties()
         {
-            _constantBuffers = new Dictionary<int, BufferDefinition>();
-            _storageBuffers = new Dictionary<int, BufferDefinition>();
-            _textures = new Dictionary<int, TextureDefinition>();
-            _images = new Dictionary<int, TextureDefinition>();
+            _constantBuffers = new Dictionary<SetBindingPair, BufferDefinition>();
+            _storageBuffers = new Dictionary<SetBindingPair, BufferDefinition>();
+            _textures = new Dictionary<SetBindingPair, TextureDefinition>();
+            _images = new Dictionary<SetBindingPair, TextureDefinition>();
             _localMemories = new Dictionary<int, MemoryDefinition>();
             _sharedMemories = new Dictionary<int, MemoryDefinition>();
         }
 
         public void AddOrUpdateConstantBuffer(BufferDefinition definition)
         {
-            _constantBuffers[definition.Binding] = definition;
+            _constantBuffers[new(definition.Set, definition.Binding)] = definition;
         }
 
         public void AddOrUpdateStorageBuffer(BufferDefinition definition)
         {
-            _storageBuffers[definition.Binding] = definition;
+            _storageBuffers[new(definition.Set, definition.Binding)] = definition;
         }
 
         public void AddOrUpdateTexture(TextureDefinition definition)
         {
-            _textures[definition.Binding] = definition;
+            _textures[new(definition.Set, definition.Binding)] = definition;
         }
 
         public void AddOrUpdateImage(TextureDefinition definition)
         {
-            _images[definition.Binding] = definition;
+            _images[new(definition.Set, definition.Binding)] = definition;
         }
 
         public int AddLocalMemory(MemoryDefinition definition)
diff --git a/src/Ryujinx.Graphics.Shader/StructuredIr/TextureDefinition.cs b/src/Ryujinx.Graphics.Shader/StructuredIr/TextureDefinition.cs
index e45c82854..200adb0b8 100644
--- a/src/Ryujinx.Graphics.Shader/StructuredIr/TextureDefinition.cs
+++ b/src/Ryujinx.Graphics.Shader/StructuredIr/TextureDefinition.cs
@@ -8,8 +8,9 @@ namespace Ryujinx.Graphics.Shader
         public SamplerType Type { get; }
         public TextureFormat Format { get; }
         public TextureUsageFlags Flags { get; }
+        public int ArraySize { get; }
 
-        public TextureDefinition(int set, int binding, string name, SamplerType type, TextureFormat format, TextureUsageFlags flags)
+        public TextureDefinition(int set, int binding, string name, SamplerType type, TextureFormat format, TextureUsageFlags flags, int arraySize = 1)
         {
             Set = set;
             Binding = binding;
@@ -17,11 +18,12 @@ namespace Ryujinx.Graphics.Shader
             Type = type;
             Format = format;
             Flags = flags;
+            ArraySize = arraySize;
         }
 
         public TextureDefinition SetFlag(TextureUsageFlags flag)
         {
-            return new TextureDefinition(Set, Binding, Name, Type, Format, Flags | flag);
+            return new TextureDefinition(Set, Binding, Name, Type, Format, Flags | flag, ArraySize);
         }
     }
 }
diff --git a/src/Ryujinx.Graphics.Shader/TextureHandle.cs b/src/Ryujinx.Graphics.Shader/TextureHandle.cs
index fc9ab2d67..ca120b889 100644
--- a/src/Ryujinx.Graphics.Shader/TextureHandle.cs
+++ b/src/Ryujinx.Graphics.Shader/TextureHandle.cs
@@ -1,3 +1,4 @@
+using Ryujinx.Graphics.Shader.Translation;
 using System;
 using System.Runtime.CompilerServices;
 
@@ -13,6 +14,11 @@ namespace Ryujinx.Graphics.Shader
 
     public static class TextureHandle
     {
+        // Maximum is actually 32 for OpenGL, but we reserve 2 textures for bindless emulation.
+        private const int MaxTexturesPerStageGl = 30;
+        private const int MaxTexturesPerStageVk = 64;
+        public const int NvnTextureBufferIndex = 2;
+
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
         public static int PackSlots(int cbufSlot0, int cbufSlot1)
         {
@@ -120,5 +126,11 @@ namespace Ryujinx.Graphics.Shader
 
             return handle;
         }
+
+        public static int GetMaxTexturesPerStage(TargetApi api)
+        {
+            // TODO: Query that value from the backend since those limits are not really fixed per API.
+            return api == TargetApi.Vulkan ? MaxTexturesPerStageVk : MaxTexturesPerStageGl;
+        }
     }
 }
diff --git a/src/Ryujinx.Graphics.Shader/Translation/HelperFunctionManager.cs b/src/Ryujinx.Graphics.Shader/Translation/HelperFunctionManager.cs
index ef2f8759d..3ed56cc4d 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/HelperFunctionManager.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/HelperFunctionManager.cs
@@ -1,6 +1,7 @@
 using Ryujinx.Graphics.Shader.IntermediateRepresentation;
 using System;
 using System.Collections.Generic;
+using System.Reflection.Metadata;
 using static Ryujinx.Graphics.Shader.IntermediateRepresentation.OperandHelper;
 
 namespace Ryujinx.Graphics.Shader.Translation
@@ -77,7 +78,9 @@ namespace Ryujinx.Graphics.Shader.Translation
                 HelperFunctionName.ConvertDoubleToFloat => GenerateConvertDoubleToFloatFunction(),
                 HelperFunctionName.ConvertFloatToDouble => GenerateConvertFloatToDoubleFunction(),
                 HelperFunctionName.TexelFetchScale => GenerateTexelFetchScaleFunction(),
+                HelperFunctionName.TexelFetchScaleBindless => GenerateTexelFetchScaleBindlessFunction(),
                 HelperFunctionName.TextureSizeUnscale => GenerateTextureSizeUnscaleFunction(),
+                HelperFunctionName.TextureSizeUnscaleBindless => GenerateTextureSizeUnscaleBindlessFunction(),
                 _ => throw new ArgumentException($"Invalid function name {functionName}"),
             };
         }
@@ -412,6 +415,29 @@ namespace Ryujinx.Graphics.Shader.Translation
             return new Function(ControlFlowGraph.Create(context.GetOperations()).Blocks, "TexelFetchScale", true, inArgumentsCount, 0);
         }
 
+        private Function GenerateTexelFetchScaleBindlessFunction()
+        {
+            EmitterContext context = new();
+
+            Operand input = Argument(0);
+            Operand nvHandle = Argument(1);
+
+            Operand scale = GetBindlessScale(context, nvHandle);
+
+            Operand scaleIsOne = context.FPCompareEqual(scale, ConstF(1f));
+            Operand lblScaleNotOne = Label();
+
+            context.BranchIfFalse(lblScaleNotOne, scaleIsOne);
+            context.Return(input);
+            context.MarkLabel(lblScaleNotOne);
+
+            Operand inputScaled2 = context.FPMultiply(context.IConvertS32ToFP32(input), scale);
+
+            context.Return(context.FP32ConvertToS32(inputScaled2));
+
+            return new Function(ControlFlowGraph.Create(context.GetOperations()).Blocks, "TexelFetchScaleBindless", true, 2, 0);
+        }
+
         private Function GenerateTextureSizeUnscaleFunction()
         {
             EmitterContext context = new();
@@ -436,6 +462,29 @@ namespace Ryujinx.Graphics.Shader.Translation
             return new Function(ControlFlowGraph.Create(context.GetOperations()).Blocks, "TextureSizeUnscale", true, 2, 0);
         }
 
+        private Function GenerateTextureSizeUnscaleBindlessFunction()
+        {
+            EmitterContext context = new();
+
+            Operand input = Argument(0);
+            Operand nvHandle = Argument(1);
+
+            Operand scale = GetBindlessScale(context, nvHandle);
+
+            Operand scaleIsOne = context.FPCompareEqual(scale, ConstF(1f));
+            Operand lblScaleNotOne = Label();
+
+            context.BranchIfFalse(lblScaleNotOne, scaleIsOne);
+            context.Return(input);
+            context.MarkLabel(lblScaleNotOne);
+
+            Operand inputUnscaled = context.FPDivide(context.IConvertS32ToFP32(input), scale);
+
+            context.Return(context.FP32ConvertToS32(inputUnscaled));
+
+            return new Function(ControlFlowGraph.Create(context.GetOperations()).Blocks, "TextureSizeUnscaleBindless", true, 2, 0);
+        }
+
         private Operand GetScaleIndex(EmitterContext context, Operand index)
         {
             switch (_stage)
@@ -448,6 +497,19 @@ namespace Ryujinx.Graphics.Shader.Translation
             }
         }
 
+        private Operand GetBindlessScale(EmitterContext context, Operand nvHandle)
+        {
+            int bindlessTableBinding = SetBindingPair.Pack(Constants.BindlessTextureSetIndex, Constants.BindlessTableBinding);
+            int bindlessScalesBinding = SetBindingPair.Pack(Constants.BindlessTextureSetIndex, Constants.BindlessScalesBinding);
+
+            Operand id = context.BitwiseAnd(nvHandle, Const(0xfffff));
+            Operand tableIndex = context.ShiftRightU32(id, Const(8));
+            Operand scaleIndex = context.Load(StorageKind.ConstantBuffer, bindlessTableBinding, Const(0), tableIndex, Const(0));
+            Operand scale = context.Load(StorageKind.ConstantBuffer, bindlessScalesBinding, Const(0), scaleIndex);
+
+            return scale;
+        }
+
         public static Operand GetBitOffset(EmitterContext context, Operand offset)
         {
             return context.ShiftLeft(context.BitwiseAnd(offset, Const(3)), Const(3));
diff --git a/src/Ryujinx.Graphics.Shader/Translation/HelperFunctionName.cs b/src/Ryujinx.Graphics.Shader/Translation/HelperFunctionName.cs
index 09b17729d..9abdd037e 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/HelperFunctionName.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/HelperFunctionName.cs
@@ -15,6 +15,8 @@ namespace Ryujinx.Graphics.Shader.Translation
         ShuffleUp,
         ShuffleXor,
         TexelFetchScale,
+        TexelFetchScaleBindless,
         TextureSizeUnscale,
+        TextureSizeUnscaleBindless,
     }
 }
diff --git a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessToIndexed.cs b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessToIndexed.cs
deleted file mode 100644
index 2bd31fe1b..000000000
--- a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessToIndexed.cs
+++ /dev/null
@@ -1,118 +0,0 @@
-using Ryujinx.Graphics.Shader.IntermediateRepresentation;
-using System.Collections.Generic;
-
-using static Ryujinx.Graphics.Shader.IntermediateRepresentation.OperandHelper;
-
-namespace Ryujinx.Graphics.Shader.Translation.Optimizations
-{
-    static class BindlessToIndexed
-    {
-        private const int NvnTextureBufferIndex = 2;
-
-        public static void RunPass(BasicBlock block, ResourceManager resourceManager)
-        {
-            // We can turn a bindless texture access into a indexed access,
-            // as long the following conditions are true:
-            // - The handle is loaded using a LDC instruction.
-            // - The handle is loaded from the constant buffer with the handles (CB2 for NVN).
-            // - The load has a constant offset.
-            // The base offset of the array of handles on the constant buffer is the constant offset.
-            for (LinkedListNode<INode> node = block.Operations.First; node != null; node = node.Next)
-            {
-                if (node.Value is not TextureOperation texOp)
-                {
-                    continue;
-                }
-
-                if ((texOp.Flags & TextureFlags.Bindless) == 0)
-                {
-                    continue;
-                }
-
-                if (texOp.GetSource(0).AsgOp is not Operation handleAsgOp)
-                {
-                    continue;
-                }
-
-                if (handleAsgOp.Inst != Instruction.Load ||
-                    handleAsgOp.StorageKind != StorageKind.ConstantBuffer ||
-                    handleAsgOp.SourcesCount != 4)
-                {
-                    continue;
-                }
-
-                Operand ldcSrc0 = handleAsgOp.GetSource(0);
-
-                if (ldcSrc0.Type != OperandType.Constant ||
-                    !resourceManager.TryGetConstantBufferSlot(ldcSrc0.Value, out int src0CbufSlot) ||
-                    src0CbufSlot != NvnTextureBufferIndex)
-                {
-                    continue;
-                }
-
-                Operand ldcSrc1 = handleAsgOp.GetSource(1);
-
-                // We expect field index 0 to be accessed.
-                if (ldcSrc1.Type != OperandType.Constant || ldcSrc1.Value != 0)
-                {
-                    continue;
-                }
-
-                Operand ldcSrc2 = handleAsgOp.GetSource(2);
-
-                // FIXME: This is missing some checks, for example, a check to ensure that the shift value is 2.
-                // Might be not worth fixing since if that doesn't kick in, the result will be no texture
-                // to access anyway which is also wrong.
-                // Plus this whole transform is fundamentally flawed as-is since we have no way to know the array size.
-                // Eventually, this should be entirely removed in favor of a implementation that supports true bindless
-                // texture access.
-                if (ldcSrc2.AsgOp is not Operation shrOp || shrOp.Inst != Instruction.ShiftRightU32)
-                {
-                    continue;
-                }
-
-                if (shrOp.GetSource(0).AsgOp is not Operation shrOp2 || shrOp2.Inst != Instruction.ShiftRightU32)
-                {
-                    continue;
-                }
-
-                if (shrOp2.GetSource(0).AsgOp is not Operation addOp || addOp.Inst != Instruction.Add)
-                {
-                    continue;
-                }
-
-                Operand addSrc1 = addOp.GetSource(1);
-
-                if (addSrc1.Type != OperandType.Constant)
-                {
-                    continue;
-                }
-
-                TurnIntoIndexed(resourceManager, texOp, addSrc1.Value / 4);
-
-                Operand index = Local();
-
-                Operand source = addOp.GetSource(0);
-
-                Operation shrBy3 = new(Instruction.ShiftRightU32, index, source, Const(3));
-
-                block.Operations.AddBefore(node, shrBy3);
-
-                texOp.SetSource(0, index);
-            }
-        }
-
-        private static void TurnIntoIndexed(ResourceManager resourceManager, TextureOperation texOp, int handle)
-        {
-            int binding = 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/Optimizations/Optimizer.cs b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/Optimizer.cs
index 17427a5f9..13a518a8e 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/Optimizer.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/Optimizer.cs
@@ -16,11 +16,34 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
             GlobalToStorage.RunPass(context.Hfm, context.Blocks, context.ResourceManager, context.GpuAccessor, context.TargetLanguage);
 
             bool hostSupportsShaderFloat64 = context.GpuAccessor.QueryHostSupportsShaderFloat64();
+            int textureBufferIndex = context.GpuAccessor.QueryTextureBufferIndex();
 
             // Those passes are looking for specific patterns and only needs to run once.
             for (int blkIndex = 0; blkIndex < context.Blocks.Length; blkIndex++)
             {
-                BindlessToIndexed.RunPass(context.Blocks[blkIndex], context.ResourceManager);
+                if (textureBufferIndex == TextureHandle.NvnTextureBufferIndex)
+                {
+                    BasicBlock block = context.Blocks[blkIndex];
+
+                    for (LinkedListNode<INode> node = block.Operations.First; node != null; node = node.Next)
+                    {
+                        for (int index = 0; index < node.Value.SourcesCount; index++)
+                        {
+                            Operand src = node.Value.GetSource(index);
+
+                            // The shader accessing constant buffer 2 is an indication that
+                            // the bindless access is for separate texture/sampler combinations.
+                            // Bindless elimination should be able to take care of that, but if it doesn't,
+                            // we still don't want to use full bindless for those cases
+                            if (src.Type == OperandType.ConstantBuffer && src.GetCbufSlot() == textureBufferIndex)
+                            {
+                                context.BindlessTexturesAllowed = false;
+                                break;
+                            }
+                        }
+                    }
+                }
+
                 BindlessElimination.RunPass(context.Blocks[blkIndex], context.ResourceManager, context.GpuAccessor);
 
                 // FragmentCoord only exists on fragment shaders, so we don't need to check other stages.
diff --git a/src/Ryujinx.Graphics.Shader/Translation/ResourceManager.cs b/src/Ryujinx.Graphics.Shader/Translation/ResourceManager.cs
index 83332711f..a27fae2cc 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/ResourceManager.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/ResourceManager.cs
@@ -14,9 +14,6 @@ namespace Ryujinx.Graphics.Shader.Translation
         private const int DefaultLocalMemorySize = 128;
         private const int DefaultSharedMemorySize = 4096;
 
-        // 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;
@@ -28,11 +25,10 @@ namespace Ryujinx.Graphics.Shader.Translation
         private uint _sbSlotWritten;
 
         private readonly Dictionary<int, int> _sbSlots;
-        private readonly Dictionary<int, int> _sbSlotsReverse;
 
         private readonly HashSet<int> _usedConstantBufferBindings;
 
-        private readonly record struct TextureInfo(int CbufSlot, int Handle, bool Indexed, TextureFormat Format);
+        private readonly record struct TextureInfo(int CbufSlot, int Handle, TextureFormat Format);
 
         private struct TextureMeta
         {
@@ -73,7 +69,6 @@ namespace Ryujinx.Graphics.Shader.Translation
             _sbSlotToBindingMap.AsSpan().Fill(-1);
 
             _sbSlots = new();
-            _sbSlotsReverse = new();
 
             _usedConstantBufferBindings = new();
 
@@ -147,6 +142,68 @@ namespace Ryujinx.Graphics.Shader.Translation
             return Properties.AddLocalMemory(new MemoryDefinition(name, type, arrayLength));
         }
 
+        public void EnsureBindlessBinding(TargetApi targetApi, SamplerType samplerType, bool isImage)
+        {
+            if (targetApi == TargetApi.Vulkan)
+            {
+                Properties.AddOrUpdateConstantBuffer(new BufferDefinition(
+                    BufferLayout.Std140,
+                    Constants.BindlessTextureSetIndex,
+                    Constants.BindlessTableBinding,
+                    "bindless_table",
+                    new StructureType(new[] { new StructureField(AggregateType.Array | AggregateType.Vector2 | AggregateType.U32, "table", 0x1000) })));
+
+                Properties.AddOrUpdateStorageBuffer(new BufferDefinition(
+                    BufferLayout.Std430,
+                    Constants.BindlessTextureSetIndex,
+                    Constants.BindlessScalesBinding,
+                    "bindless_scales",
+                    new StructureType(new[] { new StructureField(AggregateType.Array | AggregateType.FP32, "scales", 0) })));
+
+                if (isImage)
+                {
+                    string name = $"bindless_{samplerType.ToGlslImageType(AggregateType.FP32)}";
+
+                    if (samplerType == SamplerType.TextureBuffer)
+                    {
+                        AddBindlessDefinition(8, 0, name, SamplerType.TextureBuffer);
+                    }
+                    else
+                    {
+                        AddBindlessDefinition(7, 0, name, samplerType);
+                    }
+                }
+                else
+                {
+                    string name = $"bindless_{(samplerType & ~SamplerType.Shadow).ToGlslSamplerType()}";
+
+                    if (samplerType == SamplerType.TextureBuffer)
+                    {
+                        AddBindlessSeparateDefinition(5, 0, name, SamplerType.TextureBuffer);
+                    }
+                    else
+                    {
+                        AddBindlessSeparateDefinition(4, 2, name, samplerType);
+                    }
+
+                    // Sampler
+                    AddBindlessDefinition(6, 0, "bindless_samplers", SamplerType.None);
+                }
+            }
+        }
+
+        private void AddBindlessDefinition(int set, int binding, string name, SamplerType samplerType)
+        {
+            TextureDefinition definition = new(set, binding, name, samplerType, TextureFormat.Unknown, TextureUsageFlags.None, 0);
+            Properties.AddOrUpdateTexture(definition);
+        }
+
+        private void AddBindlessSeparateDefinition(int set, int binding, string name, SamplerType samplerType)
+        {
+            samplerType = (samplerType & ~SamplerType.Shadow) | SamplerType.Separate;
+            AddBindlessDefinition(set, binding, name, samplerType);
+        }
+
         public int GetConstantBufferBinding(int slot)
         {
             int binding = _cbSlotToBindingMap[slot];
@@ -158,7 +215,7 @@ namespace Ryujinx.Graphics.Shader.Translation
                 AddNewConstantBuffer(binding, $"{_stagePrefix}_c{slotNumber}");
             }
 
-            return binding;
+            return SetBindingPair.Pack(Constants.VkConstantBufferSetIndex, binding);
         }
 
         public bool TryGetStorageBufferBinding(int sbCbSlot, int sbCbOffset, bool write, out int binding)
@@ -166,6 +223,7 @@ namespace Ryujinx.Graphics.Shader.Translation
             if (!TryGetSbSlot((byte)sbCbSlot, (ushort)sbCbOffset, out int slot))
             {
                 binding = 0;
+
                 return false;
             }
 
@@ -179,6 +237,8 @@ namespace Ryujinx.Graphics.Shader.Translation
                 AddNewStorageBuffer(binding, $"{_stagePrefix}_s{slotNumber}");
             }
 
+            binding = SetBindingPair.Pack(Constants.VkStorageBufferSetIndex, binding);
+
             if (write)
             {
                 _sbSlotWritten |= 1u << slot;
@@ -201,7 +261,6 @@ namespace Ryujinx.Graphics.Shader.Translation
                 }
 
                 _sbSlots.Add(key, slot);
-                _sbSlotsReverse.Add(slot, key);
             }
 
             return true;
@@ -211,7 +270,7 @@ namespace Ryujinx.Graphics.Shader.Translation
         {
             for (slot = 0; slot < _cbSlotToBindingMap.Length; slot++)
             {
-                if (_cbSlotToBindingMap[slot] == binding)
+                if (SetBindingPair.Pack(Constants.VkConstantBufferSetIndex, _cbSlotToBindingMap[slot]) == binding)
                 {
                     return true;
                 }
@@ -245,7 +304,7 @@ namespace Ryujinx.Graphics.Shader.Translation
 
             _gpuAccessor.RegisterTexture(handle, cbufSlot);
 
-            return binding;
+            return SetBindingPair.Pack(isImage ? Constants.VkImageSetIndex : Constants.VkTextureSetIndex, binding);
         }
 
         private int GetTextureOrImageBinding(
@@ -260,7 +319,6 @@ namespace Ryujinx.Graphics.Shader.Translation
             bool coherent)
         {
             var dimensions = type.GetDimensions();
-            var isIndexed = type.HasFlag(SamplerType.Indexed);
             var dict = isImage ? _usedImages : _usedTextures;
 
             var usageFlags = TextureUsageFlags.None;
@@ -269,7 +327,7 @@ namespace Ryujinx.Graphics.Shader.Translation
             {
                 usageFlags |= TextureUsageFlags.NeedsScaleValue;
 
-                var canScale = _stage.SupportsRenderScale() && !isIndexed && !write && dimensions == 2;
+                var canScale = _stage.SupportsRenderScale() && !write && dimensions == 2;
 
                 if (!canScale)
                 {
@@ -289,76 +347,65 @@ namespace Ryujinx.Graphics.Shader.Translation
                 usageFlags |= TextureUsageFlags.ImageCoherent;
             }
 
-            int arraySize = isIndexed ? SamplerArraySize : 1;
-            int firstBinding = -1;
-
-            for (int layer = 0; layer < arraySize; layer++)
+            var info = new TextureInfo(cbufSlot, handle, format);
+            var meta = new TextureMeta()
             {
-                var info = new TextureInfo(cbufSlot, handle + layer * 2, isIndexed, format);
-                var meta = new TextureMeta()
-                {
-                    AccurateType = accurateType,
-                    Type = type,
-                    UsageFlags = usageFlags,
-                };
+                AccurateType = accurateType,
+                Type = type,
+                UsageFlags = usageFlags
+            };
 
-                int binding;
+            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;
+            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);
+                binding = isImage
+                    ? _gpuAccessor.QueryBindingImage(dict.Count, isBuffer)
+                    : _gpuAccessor.QueryBindingTexture(dict.Count, isBuffer);
 
-                    meta.Binding = binding;
+                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(definition);
-                }
-                else
-                {
-                    Properties.AddOrUpdateTexture(definition);
-                }
-
-                if (layer == 0)
-                {
-                    firstBinding = binding;
-                }
+                dict.Add(info, meta);
             }
 
-            return firstBinding;
+            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(definition);
+            }
+            else
+            {
+                Properties.AddOrUpdateTexture(definition);
+            }
+
+            return binding;
         }
 
         private static TextureMeta MergeTextureMeta(TextureMeta meta, TextureMeta existingMeta)
@@ -385,7 +432,7 @@ namespace Ryujinx.Graphics.Shader.Translation
 
             foreach ((TextureInfo info, TextureMeta meta) in _usedTextures)
             {
-                if (meta.Binding == binding)
+                if (SetBindingPair.Pack(Constants.VkTextureSetIndex, meta.Binding) == binding)
                 {
                     selectedInfo = info;
                     selectedMeta = meta;
@@ -399,8 +446,7 @@ namespace Ryujinx.Graphics.Shader.Translation
                 selectedMeta.UsageFlags |= TextureUsageFlags.NeedsScaleValue;
 
                 var dimensions = type.GetDimensions();
-                var isIndexed = type.HasFlag(SamplerType.Indexed);
-                var canScale = _stage.SupportsRenderScale() && !isIndexed && dimensions == 2;
+                var canScale = _stage.SupportsRenderScale() && dimensions == 2;
 
                 if (!canScale)
                 {
@@ -428,7 +474,7 @@ namespace Ryujinx.Graphics.Shader.Translation
             {
                 int binding = _cbSlotToBindingMap[slot];
 
-                if (binding >= 0 && _usedConstantBufferBindings.Contains(binding))
+                if (binding >= 0 && _usedConstantBufferBindings.Contains(SetBindingPair.Pack(Constants.VkConstantBufferSetIndex, binding)))
                 {
                     descriptors[descriptorIndex++] = new BufferDescriptor(binding, slot);
                 }
@@ -502,7 +548,7 @@ namespace Ryujinx.Graphics.Shader.Translation
         {
             foreach ((TextureInfo info, TextureMeta meta) in _usedTextures)
             {
-                if (meta.Binding == binding)
+                if (SetBindingPair.Pack(Constants.VkTextureSetIndex, meta.Binding) == binding)
                 {
                     cbufSlot = info.CbufSlot;
                     handle = info.Handle;
@@ -516,19 +562,19 @@ namespace Ryujinx.Graphics.Shader.Translation
             return false;
         }
 
-        private static int FindDescriptorIndex(TextureDescriptor[] array, int binding)
+        private static int FindDescriptorIndex(TextureDescriptor[] array, int setIndex, int binding)
         {
-            return Array.FindIndex(array, x => x.Binding == binding);
+            return Array.FindIndex(array, x => SetBindingPair.Pack(setIndex, x.Binding) == binding);
         }
 
         public int FindTextureDescriptorIndex(int binding)
         {
-            return FindDescriptorIndex(GetTextureDescriptors(), binding);
+            return FindDescriptorIndex(GetTextureDescriptors(), Constants.VkTextureSetIndex, binding);
         }
 
         public int FindImageDescriptorIndex(int binding)
         {
-            return FindDescriptorIndex(GetImageDescriptors(), binding);
+            return FindDescriptorIndex(GetImageDescriptors(), Constants.VkImageSetIndex, binding);
         }
 
         private void AddNewConstantBuffer(int binding, string name)
diff --git a/src/Ryujinx.Graphics.Shader/Translation/ResourceReservations.cs b/src/Ryujinx.Graphics.Shader/Translation/ResourceReservations.cs
index d559f6699..5c211e38c 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/ResourceReservations.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/ResourceReservations.cs
@@ -32,12 +32,12 @@ namespace Ryujinx.Graphics.Shader.Translation
         private readonly Dictionary<IoDefinition, int> _offsets;
         internal IReadOnlyDictionary<IoDefinition, int> Offsets => _offsets;
 
-        internal ResourceReservations(bool isTransformFeedbackEmulated, bool vertexAsCompute)
+        internal ResourceReservations(TargetApi targetApi, bool isTransformFeedbackEmulated, bool vertexAsCompute)
         {
             // All stages reserves the first constant buffer binding for the support buffer.
             ReservedConstantBuffers = 1;
             ReservedStorageBuffers = 0;
-            ReservedTextures = 0;
+            ReservedTextures = targetApi == TargetApi.OpenGL ? 2 : 0; // Reserve 2 texture bindings on OpenGL for bindless emulation.
             ReservedImages = 0;
 
             if (isTransformFeedbackEmulated)
@@ -71,10 +71,11 @@ namespace Ryujinx.Graphics.Shader.Translation
 
         internal ResourceReservations(
             IGpuAccessor gpuAccessor,
+            TargetApi targetApi,
             bool isTransformFeedbackEmulated,
             bool vertexAsCompute,
             IoUsage? vacInput,
-            IoUsage vacOutput) : this(isTransformFeedbackEmulated, vertexAsCompute)
+            IoUsage vacOutput) : this(targetApi, isTransformFeedbackEmulated, vertexAsCompute)
         {
             if (vertexAsCompute)
             {
diff --git a/src/Ryujinx.Graphics.Shader/Translation/TransformContext.cs b/src/Ryujinx.Graphics.Shader/Translation/TransformContext.cs
index 87ebb8e7c..794b975b0 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/TransformContext.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/TransformContext.cs
@@ -9,9 +9,12 @@ namespace Ryujinx.Graphics.Shader.Translation
         public readonly ShaderDefinitions Definitions;
         public readonly ResourceManager ResourceManager;
         public readonly IGpuAccessor GpuAccessor;
+        public readonly TargetApi TargetApi;
         public readonly TargetLanguage TargetLanguage;
         public readonly ShaderStage Stage;
         public readonly ref FeatureFlags UsedFeatures;
+        public readonly ref BindlessTextureFlags BindlessTextureFlags;
+        public readonly ref bool BindlessTexturesAllowed;
 
         public TransformContext(
             HelperFunctionManager hfm,
@@ -19,18 +22,24 @@ namespace Ryujinx.Graphics.Shader.Translation
             ShaderDefinitions definitions,
             ResourceManager resourceManager,
             IGpuAccessor gpuAccessor,
+            TargetApi targetApi,
             TargetLanguage targetLanguage,
             ShaderStage stage,
-            ref FeatureFlags usedFeatures)
+            ref FeatureFlags usedFeatures,
+            ref BindlessTextureFlags bindlessTextureFlags,
+            ref bool bindlessTexturesAllowed)
         {
             Hfm = hfm;
             Blocks = blocks;
             Definitions = definitions;
             ResourceManager = resourceManager;
             GpuAccessor = gpuAccessor;
+            TargetApi = targetApi;
             TargetLanguage = targetLanguage;
             Stage = stage;
             UsedFeatures = ref usedFeatures;
+            BindlessTextureFlags = ref bindlessTextureFlags;
+            BindlessTexturesAllowed = ref bindlessTexturesAllowed;
         }
     }
 }
diff --git a/src/Ryujinx.Graphics.Shader/Translation/Transforms/TexturePass.cs b/src/Ryujinx.Graphics.Shader/Translation/Transforms/TexturePass.cs
index 495ea8a94..94c20cded 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/Transforms/TexturePass.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/Transforms/TexturePass.cs
@@ -16,12 +16,26 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
         {
             if (node.Value is TextureOperation texOp)
             {
-                node = InsertTexelFetchScale(context.Hfm, node, context.ResourceManager, context.Stage);
-                node = InsertTextureSizeUnscale(context.Hfm, node, context.ResourceManager, context.Stage);
+                LinkedListNode<INode> prevNode = node;
+                node = TurnIntoBindlessIfExceeding(
+                    node,
+                    context.ResourceManager,
+                    context.TargetApi,
+                    ref context.BindlessTextureFlags,
+                    context.BindlessTexturesAllowed,
+                    context.GpuAccessor.QueryTextureBufferIndex());
+
+                if (prevNode != node)
+                {
+                    return node;
+                }
+
+                node = InsertTexelFetchScale(context.Hfm, node, context.ResourceManager, context.Stage, context.TargetApi);
+                node = InsertTextureSizeUnscale(context.Hfm, node, context.ResourceManager, context.Stage, context.TargetApi);
 
                 if (texOp.Inst == Instruction.TextureSample)
                 {
-                    node = InsertCoordNormalization(context.Hfm, node, context.ResourceManager, context.GpuAccessor, context.Stage);
+                    node = InsertCoordNormalization(context.Hfm, node, context.ResourceManager, context.GpuAccessor, context.Stage, context.TargetApi);
                     node = InsertCoordGatherBias(node, context.ResourceManager, context.GpuAccessor);
                     node = InsertConstOffsets(node, context.GpuAccessor, context.Stage);
 
@@ -39,31 +53,41 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
             HelperFunctionManager hfm,
             LinkedListNode<INode> node,
             ResourceManager resourceManager,
-            ShaderStage stage)
+            ShaderStage stage,
+            TargetApi targetApi)
         {
             TextureOperation texOp = (TextureOperation)node.Value;
 
             bool isBindless = (texOp.Flags & TextureFlags.Bindless) != 0;
             bool intCoords = (texOp.Flags & TextureFlags.IntCoords) != 0;
-            bool isIndexed = (texOp.Type & SamplerType.Indexed) != 0;
 
             int coordsCount = texOp.Type.GetDimensions();
 
-            int coordsIndex = isBindless || isIndexed ? 1 : 0;
+            int coordsIndex = isBindless ? 1 : 0;
 
             bool isImage = IsImageInstructionWithScale(texOp.Inst);
 
             if ((texOp.Inst == Instruction.TextureSample || isImage) &&
                 (intCoords || isImage) &&
-                !isBindless &&
-                !isIndexed &&
+                (!isBindless || targetApi == TargetApi.Vulkan) && // TODO: OpenGL support.
                 stage.SupportsRenderScale() &&
                 TypeSupportsScale(texOp.Type))
             {
-                int functionId = hfm.GetOrCreateFunctionId(HelperFunctionName.TexelFetchScale);
-                int samplerIndex = isImage
-                    ? resourceManager.GetTextureDescriptors().Length + resourceManager.FindImageDescriptorIndex(texOp.Binding)
-                    : resourceManager.FindTextureDescriptorIndex(texOp.Binding);
+                int functionId;
+                Operand samplerIndex;
+
+                if (isBindless)
+                {
+                    functionId = hfm.GetOrCreateFunctionId(HelperFunctionName.TexelFetchScaleBindless);
+                    samplerIndex = texOp.GetSource(0);
+                }
+                else
+                {
+                    functionId = hfm.GetOrCreateFunctionId(HelperFunctionName.TexelFetchScale);
+                    samplerIndex = isImage
+                        ? Const(resourceManager.GetTextureDescriptors().Length + resourceManager.FindImageDescriptorIndex(texOp.Binding))
+                        : Const(resourceManager.FindTextureDescriptorIndex(texOp.Binding));
+                }
 
                 for (int index = 0; index < coordsCount; index++)
                 {
@@ -72,11 +96,11 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
 
                     if (stage == ShaderStage.Fragment)
                     {
-                        callArgs = new Operand[] { Const(functionId), texOp.GetSource(coordsIndex + index), Const(samplerIndex), Const(index) };
+                        callArgs = new Operand[] { Const(functionId), texOp.GetSource(coordsIndex + index), samplerIndex, Const(index) };
                     }
                     else
                     {
-                        callArgs = new Operand[] { Const(functionId), texOp.GetSource(coordsIndex + index), Const(samplerIndex) };
+                        callArgs = new Operand[] { Const(functionId), texOp.GetSource(coordsIndex + index), samplerIndex };
                     }
 
                     node.List.AddBefore(node, new Operation(Instruction.Call, 0, scaledCoord, callArgs));
@@ -92,22 +116,32 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
             HelperFunctionManager hfm,
             LinkedListNode<INode> node,
             ResourceManager resourceManager,
-            ShaderStage stage)
+            ShaderStage stage,
+            TargetApi targetApi)
         {
             TextureOperation texOp = (TextureOperation)node.Value;
 
             bool isBindless = (texOp.Flags & TextureFlags.Bindless) != 0;
-            bool isIndexed = (texOp.Type & SamplerType.Indexed) != 0;
 
             if (texOp.Inst == Instruction.TextureQuerySize &&
                 texOp.Index < 2 &&
-                !isBindless &&
-                !isIndexed &&
+                (!isBindless || targetApi == TargetApi.Vulkan) && // TODO: OpenGL support.
                 stage.SupportsRenderScale() &&
                 TypeSupportsScale(texOp.Type))
             {
-                int functionId = hfm.GetOrCreateFunctionId(HelperFunctionName.TextureSizeUnscale);
-                int samplerIndex = resourceManager.FindTextureDescriptorIndex(texOp.Binding);
+                int functionId;
+                Operand samplerIndex;
+
+                if (isBindless)
+                {
+                    functionId = hfm.GetOrCreateFunctionId(HelperFunctionName.TextureSizeUnscaleBindless);
+                    samplerIndex = texOp.GetSource(0);
+                }
+                else
+                {
+                    functionId = hfm.GetOrCreateFunctionId(HelperFunctionName.TextureSizeUnscale);
+                    samplerIndex = Const(resourceManager.FindTextureDescriptorIndex(texOp.Binding));
+                }
 
                 for (int index = texOp.DestsCount - 1; index >= 0; index--)
                 {
@@ -128,7 +162,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
                         }
                     }
 
-                    Operand[] callArgs = new Operand[] { Const(functionId), dest, Const(samplerIndex) };
+                    Operand[] callArgs = new Operand[] { Const(functionId), dest, samplerIndex };
 
                     node.List.AddAfter(node, new Operation(Instruction.Call, 0, unscaledSize, callArgs));
                 }
@@ -142,7 +176,8 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
             LinkedListNode<INode> node,
             ResourceManager resourceManager,
             IGpuAccessor gpuAccessor,
-            ShaderStage stage)
+            ShaderStage stage,
+            TargetApi targetApi)
         {
             // 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,
@@ -167,10 +202,8 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
                 return node;
             }
 
-            bool isIndexed = (texOp.Type & SamplerType.Indexed) != 0;
-
             int coordsCount = texOp.Type.GetDimensions();
-            int coordsIndex = isBindless || isIndexed ? 1 : 0;
+            int coordsIndex = isBindless ? 1 : 0;
 
             int normCoordsCount = (texOp.Type & SamplerType.Mask) == SamplerType.TextureCube ? 2 : coordsCount;
 
@@ -180,7 +213,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
 
                 Operand[] texSizeSources;
 
-                if (isBindless || isIndexed)
+                if (isBindless)
                 {
                     texSizeSources = new Operand[] { texOp.GetSource(0), Const(0) };
                 }
@@ -209,7 +242,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
 
                 texOp.SetSource(coordsIndex + index, coordNormalized);
 
-                InsertTextureSizeUnscale(hfm, textureSizeNode, resourceManager, stage);
+                InsertTextureSizeUnscale(hfm, textureSizeNode, resourceManager, stage, targetApi);
             }
 
             return node;
@@ -234,10 +267,8 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
                 return node;
             }
 
-            bool isIndexed = (texOp.Type & SamplerType.Indexed) != 0;
-
             int coordsCount = texOp.Type.GetDimensions();
-            int coordsIndex = isBindless || isIndexed ? 1 : 0;
+            int coordsIndex = isBindless ? 1 : 0;
 
             int normCoordsCount = (texOp.Type & SamplerType.Mask) == SamplerType.TextureCube ? 2 : coordsCount;
 
@@ -249,7 +280,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
 
                 Operand[] texSizeSources;
 
-                if (isBindless || isIndexed)
+                if (isBindless)
                 {
                     texSizeSources = new Operand[] { texOp.GetSource(0), Const(0) };
                 }
@@ -321,7 +352,6 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
             bool hasLodLevel = (texOp.Flags & TextureFlags.LodLevel) != 0;
 
             bool isArray = (texOp.Type & SamplerType.Array) != 0;
-            bool isIndexed = (texOp.Type & SamplerType.Indexed) != 0;
             bool isMultisample = (texOp.Type & SamplerType.Multisample) != 0;
             bool isShadow = (texOp.Type & SamplerType.Shadow) != 0;
 
@@ -347,7 +377,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
 
             int copyCount = 0;
 
-            if (isBindless || isIndexed)
+            if (isBindless)
             {
                 copyCount++;
             }
@@ -424,7 +454,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
                 sources[dstIndex++] = texOp.GetSource(srcIndex++);
             }
 
-            int coordsIndex = isBindless || isIndexed ? 1 : 0;
+            int coordsIndex = isBindless ? 1 : 0;
 
             int componentIndex = texOp.Index;
 
@@ -435,7 +465,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
                 dests[i] = texOp.GetDest(i);
             }
 
-            Operand bindlessHandle = isBindless || isIndexed ? sources[0] : null;
+            Operand bindlessHandle = isBindless ? sources[0] : null;
 
             LinkedListNode<INode> oldNode = node;
 
@@ -748,5 +778,113 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
         {
             return (type & SamplerType.Mask) == SamplerType.Texture2D;
         }
+
+        private static LinkedListNode<INode> TurnIntoBindlessIfExceeding(
+            LinkedListNode<INode> node,
+            ResourceManager resourceManager,
+            TargetApi targetApi,
+            ref BindlessTextureFlags bindlessTextureFlags,
+            bool bindlessTexturesAllowed,
+            int textureBufferIndex)
+        {
+            if (node.Value is not TextureOperation texOp)
+            {
+                return node;
+            }
+
+            // If it's already bindless, then we have nothing to do.
+            if (texOp.Flags.HasFlag(TextureFlags.Bindless))
+            {
+                resourceManager.EnsureBindlessBinding(targetApi, texOp.Type, texOp.Inst.IsImage());
+
+                if (IsIndexedAccess(resourceManager, texOp, textureBufferIndex))
+                {
+                    bindlessTextureFlags |= BindlessTextureFlags.BindlessNvn;
+                    return node;
+                }
+
+                if (bindlessTexturesAllowed)
+                {
+                    bindlessTextureFlags |= BindlessTextureFlags.BindlessFull;
+                    return node;
+                }
+                else
+                {
+                    // Set any destination operand to zero and remove the texture access.
+                    // This is a case where bindless elimination failed, and we assume
+                    // it's too risky to try using full bindless emulation.
+
+                    for (int destIndex = 0; destIndex < texOp.DestsCount; destIndex++)
+                    {
+                        Operand dest = texOp.GetDest(destIndex);
+                        node.List.AddBefore(node, new Operation(Instruction.Copy, dest, Const(0)));
+                    }
+
+                    LinkedListNode<INode> prevNode = node.Previous;
+                    node.List.Remove(node);
+
+                    return prevNode;
+                }
+            }
+
+            // If the index is within the host API limits, then we don't need to make it bindless.
+            int index = resourceManager.FindTextureDescriptorIndex(texOp.Binding);
+            if (index < TextureHandle.GetMaxTexturesPerStage(targetApi))
+            {
+                return node;
+            }
+
+            TextureDescriptor descriptor = resourceManager.GetTextureDescriptors()[index];
+
+            (int textureWordOffset, int samplerWordOffset, TextureHandleType handleType) = TextureHandle.UnpackOffsets(descriptor.HandleIndex);
+            (int textureCbufSlot, int samplerCbufSlot) = TextureHandle.UnpackSlots(descriptor.CbufSlot, textureBufferIndex);
+
+            Operand handle = Cbuf(textureCbufSlot, textureWordOffset);
+
+            if (handleType != TextureHandleType.CombinedSampler)
+            {
+                Operand handle2 = Cbuf(samplerCbufSlot, samplerWordOffset);
+
+                if (handleType == TextureHandleType.SeparateSamplerId)
+                {
+                    Operand temp = Local();
+                    node.List.AddBefore(node, new Operation(Instruction.ShiftLeft, temp, handle2, Const(20)));
+                    handle2 = temp;
+                }
+
+                Operand handleCombined = Local();
+                node.List.AddBefore(node, new Operation(Instruction.BitwiseOr, handleCombined, handle, handle2));
+                handle = handleCombined;
+            }
+
+            texOp.TurnIntoBindless(handle);
+            bindlessTextureFlags |= BindlessTextureFlags.BindlessConverted;
+
+            resourceManager.EnsureBindlessBinding(targetApi, texOp.Type, texOp.Inst.IsImage());
+
+            return node;
+        }
+
+        private static bool IsIndexedAccess(ResourceManager resourceManager, TextureOperation texOp, int textureBufferIndex)
+        {
+            // Try to detect a indexed access.
+            // The access is considered indexed if the handle is loaded with a LDC instruction
+            // from the driver reserved constant buffer used for texture handles.
+            if (!(texOp.GetSource(0).AsgOp is Operation handleAsgOp))
+            {
+                return false;
+            }
+
+            if (handleAsgOp.Inst != Instruction.Load || handleAsgOp.StorageKind != StorageKind.ConstantBuffer)
+            {
+                return false;
+            }
+
+            Operand ldcSrc0 = handleAsgOp.GetSource(0);
+
+            return ldcSrc0.Type == OperandType.Constant &&
+                   resourceManager.TryGetConstantBufferSlot(ldcSrc0.Value, out int cbSlot) &&
+                   cbSlot == textureBufferIndex;
+        }
     }
 }
diff --git a/src/Ryujinx.Graphics.Shader/Translation/Transforms/VectorComponentSelect.cs b/src/Ryujinx.Graphics.Shader/Translation/Transforms/VectorComponentSelect.cs
index e55f4355d..2b72ccd2e 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/Transforms/VectorComponentSelect.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/Transforms/VectorComponentSelect.cs
@@ -35,7 +35,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
                 return node;
             }
 
-            BufferDefinition buffer = context.ResourceManager.Properties.ConstantBuffers[bindingIndex.Value];
+            BufferDefinition buffer = context.ResourceManager.Properties.ConstantBuffers[SetBindingPair.Unpack(bindingIndex.Value)];
             StructureField field = buffer.Type.Fields[fieldIndex.Value];
 
             int elemCount = (field.Type & AggregateType.ElementCountMask) switch
diff --git a/src/Ryujinx.Graphics.Shader/Translation/TranslatorContext.cs b/src/Ryujinx.Graphics.Shader/Translation/TranslatorContext.cs
index a112991e9..0865b1a5e 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/TranslatorContext.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/TranslatorContext.cs
@@ -264,6 +264,9 @@ namespace Ryujinx.Graphics.Shader.Translation
 
             HelperFunctionManager hfm = new(funcs, Definitions.Stage);
 
+            BindlessTextureFlags bindlessTextureFlags = BindlessTextureFlags.None;
+            bool bindlessTexturesAllowed = true;
+
             for (int i = 0; i < functions.Length; i++)
             {
                 var cfg = cfgs[i];
@@ -294,9 +297,12 @@ namespace Ryujinx.Graphics.Shader.Translation
                         Definitions,
                         resourceManager,
                         GpuAccessor,
+                        Options.TargetApi,
                         Options.TargetLanguage,
                         Definitions.Stage,
-                        ref usedFeatures);
+                        ref usedFeatures,
+                        ref bindlessTextureFlags,
+                        ref bindlessTexturesAllowed);
 
                     Optimizer.RunPass(context);
                     TransformPasses.RunPass(context);
@@ -312,6 +318,7 @@ namespace Ryujinx.Graphics.Shader.Translation
                 Definitions,
                 resourceManager,
                 usedFeatures,
+                bindlessTextureFlags,
                 clipDistancesWritten);
         }
 
@@ -322,6 +329,7 @@ namespace Ryujinx.Graphics.Shader.Translation
             ShaderDefinitions originalDefinitions,
             ResourceManager resourceManager,
             FeatureFlags usedFeatures,
+            BindlessTextureFlags bindlessTextureFlags,
             byte clipDistancesWritten)
         {
             var sInfo = StructuredProgram.MakeStructuredProgram(
@@ -345,6 +353,7 @@ namespace Ryujinx.Graphics.Shader.Translation
                 resourceManager.GetTextureDescriptors(),
                 resourceManager.GetImageDescriptors(),
                 originalDefinitions.Stage,
+                bindlessTextureFlags,
                 geometryVerticesPerPrimitive,
                 originalDefinitions.MaxOutputVertices,
                 originalDefinitions.ThreadsPerInputPrimitive,
@@ -365,7 +374,14 @@ namespace Ryujinx.Graphics.Shader.Translation
                 GpuAccessor.QueryHostSupportsTextureShadowLod(),
                 GpuAccessor.QueryHostSupportsViewportMask());
 
-            var parameters = new CodeGenParameters(attributeUsage, definitions, resourceManager.Properties, hostCapabilities, GpuAccessor, Options.TargetApi);
+            var parameters = new CodeGenParameters(
+                attributeUsage,
+                definitions,
+                resourceManager.Properties,
+                hostCapabilities,
+                GpuAccessor,
+                Options.TargetApi,
+                bindlessTextureFlags);
 
             return Options.TargetLanguage switch
             {
@@ -474,7 +490,7 @@ namespace Ryujinx.Graphics.Shader.Translation
                 ioUsage = ioUsage.Combine(_vertexOutput);
             }
 
-            return new ResourceReservations(GpuAccessor, IsTransformFeedbackEmulated, vertexAsCompute: true, _vertexOutput, ioUsage);
+            return new ResourceReservations(GpuAccessor, Options.TargetApi, IsTransformFeedbackEmulated, vertexAsCompute: true, _vertexOutput, ioUsage);
         }
 
         public void SetVertexOutputMapForGeometryAsCompute(TranslatorContext vertexContext)
@@ -569,6 +585,7 @@ namespace Ryujinx.Graphics.Shader.Translation
                 definitions,
                 resourceManager,
                 FeatureFlags.None,
+                BindlessTextureFlags.None,
                 0);
         }
 
@@ -665,6 +682,7 @@ namespace Ryujinx.Graphics.Shader.Translation
                 definitions,
                 resourceManager,
                 FeatureFlags.RtLayer,
+                BindlessTextureFlags.None,
                 0);
         }
     }
diff --git a/src/Ryujinx.Graphics.Vulkan/BindlessManager.cs b/src/Ryujinx.Graphics.Vulkan/BindlessManager.cs
new file mode 100644
index 000000000..1389efabb
--- /dev/null
+++ b/src/Ryujinx.Graphics.Vulkan/BindlessManager.cs
@@ -0,0 +1,360 @@
+using Ryujinx.Common;
+using Ryujinx.Graphics.GAL;
+using Silk.NET.Vulkan;
+using System;
+using System.Numerics;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Graphics.Vulkan
+{
+    class BindlessManager
+    {
+        private const int TextureIdBits = 20;
+        private const int SamplerIdBits = 12;
+        private const int TextureCapacity = 1 << TextureIdBits;
+        private const int SamplerCapacity = 1 << SamplerIdBits;
+
+        private const int TextureIdBlockShift = 8;
+        private const int TextureIdBlockMask = 0xfff;
+
+        // Note that each entry must be aligned to 16 bytes, this is a constant buffer layout restriction.
+        private const int IdMapElements = 4;
+
+        public const uint MinimumTexturesCount = 256;
+        public const uint MinimumSamplersCount = 256;
+
+        private readonly Dictionary<int, int> _textureIdMap;
+        private readonly Dictionary<int, int> _samplerIdMap;
+        private readonly ulong[] _textureBlockBitmap;
+        private readonly ulong[] _samplerBlockBitmap;
+
+        private ITexture[] _textureRefs;
+        private float[] _textureScales;
+        private Auto<DisposableSampler>[] _samplerRefs;
+        private bool _textureScalesDirty;
+
+        public uint TexturesCount => CalculateTexturesCount();
+        public uint SamplersCount => CalculateSamplersCount();
+
+        private readonly int[] _idMap;
+        private bool _idMapDataDirty;
+
+        private BufferHolder _idMapBuffer;
+        private BufferHolder _textureScalesBuffer;
+
+        private bool _dirty;
+        private bool _hasDescriptors;
+
+        private PipelineLayout _pipelineLayout;
+        private DescriptorSetCollection _bindlessTextures;
+        private DescriptorSetCollection _bindlessSamplers;
+        private DescriptorSetCollection _bindlessImages;
+        private DescriptorSetCollection _bindlessBufferTextures;
+        private DescriptorSetCollection _bindlessBufferImages;
+
+        public BindlessManager()
+        {
+            _textureIdMap = new Dictionary<int, int>();
+            _samplerIdMap = new Dictionary<int, int>();
+            _textureBlockBitmap = new ulong[((TextureCapacity >> TextureIdBlockShift) + 63) / 64];
+            _samplerBlockBitmap = new ulong[((SamplerCapacity >> TextureIdBlockShift) + 63) / 64];
+
+            _textureRefs = Array.Empty<TextureView>();
+            _textureScales = Array.Empty<float>();
+            _samplerRefs = Array.Empty<Auto<DisposableSampler>>();
+
+            // This is actually a structure with 2 elements,
+            // texture index (X) and sampler index (Y).
+            _idMap = new int[(TextureCapacity >> TextureIdBlockShift) * IdMapElements];
+        }
+
+        private uint CalculateTexturesCount()
+        {
+            return Math.Max(MinimumTexturesCount, (uint)BitUtils.Pow2RoundUp(_textureRefs.Length));
+        }
+
+        private uint CalculateSamplersCount()
+        {
+            return Math.Max(MinimumSamplersCount, (uint)BitUtils.Pow2RoundUp(_samplerRefs.Length));
+        }
+
+        public void SetBindlessTexture(int textureId, ITexture texture, float scale)
+        {
+            int textureIndex = GetTextureBlockId(textureId);
+
+            _textureRefs[textureIndex] = texture;
+
+            if (_textureRefs.Length != _textureScales.Length)
+            {
+                Array.Resize(ref _textureScales, _textureRefs.Length);
+            }
+
+            if (_textureScales[textureIndex] != scale)
+            {
+                _textureScales[textureIndex] = scale;
+                _textureScalesDirty = true;
+            }
+
+            _dirty = true;
+        }
+
+        public void SetBindlessSampler(int samplerId, Auto<DisposableSampler> sampler)
+        {
+            int samplerIndex = GetSamplerBlockId(samplerId);
+
+            _samplerRefs[samplerIndex] = sampler;
+            _dirty = true;
+        }
+
+        private int GetTextureBlockId(int textureId)
+        {
+            return GetBlockId(textureId, 0, _textureIdMap, _textureBlockBitmap, ref _textureRefs);
+        }
+
+        private int GetSamplerBlockId(int samplerId)
+        {
+            return GetBlockId(samplerId, 1, _samplerIdMap, _samplerBlockBitmap, ref _samplerRefs);
+        }
+
+        private int GetBlockId<T>(int id, int idMapOffset, Dictionary<int, int> idMap, ulong[] bitmap, ref T[] resourceRefs)
+        {
+            int blockIndex = (id >> TextureIdBlockShift) & TextureIdBlockMask;
+
+            if (!idMap.TryGetValue(blockIndex, out int mappedIndex))
+            {
+                mappedIndex = AllocateNewBlock(bitmap);
+
+                int minLength = (mappedIndex + 1) << TextureIdBlockShift;
+
+                if (minLength > resourceRefs.Length)
+                {
+                    Array.Resize(ref resourceRefs, minLength);
+                }
+
+                _idMap[blockIndex * IdMapElements + idMapOffset] = mappedIndex << TextureIdBlockShift;
+                _idMapDataDirty = true;
+
+                idMap.Add(blockIndex, mappedIndex);
+            }
+
+            return (mappedIndex << TextureIdBlockShift) | (id & ~(TextureIdBlockMask << TextureIdBlockShift));
+        }
+
+        private static int AllocateNewBlock(ulong[] bitmap)
+        {
+            for (int index = 0; index < bitmap.Length; index++)
+            {
+                ref ulong v = ref bitmap[index];
+
+                if (v == ulong.MaxValue)
+                {
+                    continue;
+                }
+
+                int firstFreeBit = BitOperations.TrailingZeroCount(~v);
+                v |= 1UL << firstFreeBit;
+                return index * 64 + firstFreeBit;
+            }
+
+            throw new InvalidOperationException("No free space left on the texture or sampler table.");
+        }
+
+        public void UpdateAndBind(
+            VulkanRenderer gd,
+            ShaderCollection program,
+            CommandBufferScoped cbs,
+            PipelineBindPoint pbp,
+            SamplerHolder dummySampler)
+        {
+            if (!_dirty)
+            {
+                Rebind(gd, program, cbs, pbp);
+                return;
+            }
+
+            _dirty = false;
+
+            var plce = program.GetPipelineLayoutCacheEntry(gd, TexturesCount, SamplersCount);
+
+            plce.UpdateCommandBufferIndex(cbs.CommandBufferIndex);
+
+            var btDsc = plce.GetNewDescriptorSetCollection(PipelineBase.BindlessTexturesSetIndex, out _).Get(cbs);
+            var bbtDsc = plce.GetNewDescriptorSetCollection(PipelineBase.BindlessBufferTextureSetIndex, out _).Get(cbs);
+            var bsDsc = plce.GetNewDescriptorSetCollection(PipelineBase.BindlessSamplersSetIndex, out _).Get(cbs);
+            var biDsc = plce.GetNewDescriptorSetCollection(PipelineBase.BindlessImagesSetIndex, out _).Get(cbs);
+            var bbiDsc = plce.GetNewDescriptorSetCollection(PipelineBase.BindlessBufferImageSetIndex, out _).Get(cbs);
+
+            int idMapBufferSizeInBytes = _idMap.Length * sizeof(int);
+
+            if (_idMapBuffer == null)
+            {
+                _idMapBuffer = gd.BufferManager.Create(gd, idMapBufferSizeInBytes);
+            }
+
+            if (_idMapDataDirty)
+            {
+                _idMapBuffer.SetDataUnchecked(0, MemoryMarshal.Cast<int, byte>(_idMap));
+                _idMapDataDirty = false;
+            }
+
+            int textureScalesBufferSizeInBytes = _textureScales.Length * sizeof(float);
+
+            if (_textureScalesDirty)
+            {
+                if (_textureScalesBuffer == null || _textureScalesBuffer.Size != textureScalesBufferSizeInBytes)
+                {
+                    _textureScalesBuffer?.Dispose();
+                    _textureScalesBuffer = gd.BufferManager.Create(gd, textureScalesBufferSizeInBytes);
+                }
+
+                _textureScalesBuffer.SetDataUnchecked(0, MemoryMarshal.Cast<float, byte>(_textureScales));
+                _textureScalesDirty = false;
+            }
+
+            Span<DescriptorBufferInfo> uniformBuffer = stackalloc DescriptorBufferInfo[1];
+
+            uniformBuffer[0] = new DescriptorBufferInfo()
+            {
+                Offset = 0,
+                Range = (ulong)idMapBufferSizeInBytes,
+                Buffer = _idMapBuffer.GetBuffer().Get(cbs, 0, idMapBufferSizeInBytes).Value
+            };
+
+            btDsc.UpdateBuffers(0, 0, uniformBuffer, DescriptorType.UniformBuffer);
+
+            if (_textureScalesBuffer != null)
+            {
+                Span<DescriptorBufferInfo> storageBuffer = stackalloc DescriptorBufferInfo[1];
+
+                storageBuffer[0] = new DescriptorBufferInfo()
+                {
+                    Offset = 0,
+                    Range = (ulong)textureScalesBufferSizeInBytes,
+                    Buffer = _textureScalesBuffer.GetBuffer().Get(cbs, 0, textureScalesBufferSizeInBytes).Value
+                };
+
+                btDsc.UpdateBuffers(0, 1, storageBuffer, DescriptorType.StorageBuffer);
+            }
+
+            for (int i = 0; i < _textureRefs.Length; i++)
+            {
+                var texture = _textureRefs[i];
+                if (texture is TextureView view)
+                {
+                    var td = new DescriptorImageInfo()
+                    {
+                        ImageLayout = ImageLayout.General,
+                        ImageView = view.GetImageView().Get(cbs).Value
+                    };
+
+                    btDsc.UpdateImage(0, 2, i, td, DescriptorType.SampledImage);
+
+                    if (view.Info.Format.IsImageCompatible())
+                    {
+                        td = new DescriptorImageInfo()
+                        {
+                            ImageLayout = ImageLayout.General,
+                            ImageView = view.GetIdentityImageView().Get(cbs).Value
+                        };
+
+                        biDsc.UpdateImage(0, 0, i, td, DescriptorType.StorageImage);
+                    }
+                }
+                else if (texture is TextureBuffer buffer)
+                {
+                    bool isImageCompatible = buffer.Format.IsImageCompatible();
+                    var bufferView = buffer.GetBufferView(cbs, isImageCompatible);
+
+                    bbtDsc.UpdateBufferImage(0, 0, i, bufferView, DescriptorType.UniformTexelBuffer);
+
+                    if (isImageCompatible)
+                    {
+                        bbiDsc.UpdateBufferImage(0, 0, i, bufferView, DescriptorType.StorageTexelBuffer);
+                    }
+                }
+            }
+
+            for (int i = 0; i < _samplerRefs.Length; i++)
+            {
+                var sampler = _samplerRefs[i];
+                if (sampler != null)
+                {
+                    var sd = new DescriptorImageInfo()
+                    {
+                        Sampler = sampler.Get(cbs).Value
+                    };
+
+                    if (sd.Sampler.Handle == 0)
+                    {
+                        sd.Sampler = dummySampler.GetSampler().Get(cbs).Value;
+                    }
+
+                    bsDsc.UpdateImage(0, 0, i, sd, DescriptorType.Sampler);
+                }
+            }
+
+            _pipelineLayout = plce.PipelineLayout;
+            _bindlessTextures = btDsc;
+            _bindlessSamplers = bsDsc;
+            _bindlessImages = biDsc;
+            _bindlessBufferTextures = bbtDsc;
+            _bindlessBufferImages = bbiDsc;
+
+            _hasDescriptors = true;
+
+            Bind(gd, program, cbs, pbp, plce.PipelineLayout, btDsc, bsDsc, biDsc, bbtDsc, bbiDsc);
+        }
+
+        private void Rebind(VulkanRenderer gd, ShaderCollection program, CommandBufferScoped cbs, PipelineBindPoint pbp)
+        {
+            if (_hasDescriptors)
+            {
+                Bind(
+                    gd,
+                    program,
+                    cbs,
+                    pbp,
+                    _pipelineLayout,
+                    _bindlessTextures,
+                    _bindlessSamplers,
+                    _bindlessImages,
+                    _bindlessBufferTextures,
+                    _bindlessBufferImages);
+            }
+        }
+
+        private void Bind(
+            VulkanRenderer gd,
+            ShaderCollection program,
+            CommandBufferScoped cbs,
+            PipelineBindPoint pbp,
+            PipelineLayout pipelineLayout,
+            DescriptorSetCollection bindlessTextures,
+            DescriptorSetCollection bindlessSamplers,
+            DescriptorSetCollection bindlessImages,
+            DescriptorSetCollection bindlessBufferTextures,
+            DescriptorSetCollection bindlessBufferImages)
+        {
+            gd.Api.CmdBindDescriptorSets(cbs.CommandBuffer, pbp, pipelineLayout, PipelineBase.BindlessTexturesSetIndex, 1, bindlessTextures.GetSets(), 0, ReadOnlySpan<uint>.Empty);
+            gd.Api.CmdBindDescriptorSets(cbs.CommandBuffer, pbp, pipelineLayout, PipelineBase.BindlessBufferTextureSetIndex, 1, bindlessBufferTextures.GetSets(), 0, ReadOnlySpan<uint>.Empty);
+            gd.Api.CmdBindDescriptorSets(cbs.CommandBuffer, pbp, pipelineLayout, PipelineBase.BindlessSamplersSetIndex, 1, bindlessSamplers.GetSets(), 0, ReadOnlySpan<uint>.Empty);
+            gd.Api.CmdBindDescriptorSets(cbs.CommandBuffer, pbp, pipelineLayout, PipelineBase.BindlessImagesSetIndex, 1, bindlessImages.GetSets(), 0, ReadOnlySpan<uint>.Empty);
+            gd.Api.CmdBindDescriptorSets(cbs.CommandBuffer, pbp, pipelineLayout, PipelineBase.BindlessBufferImageSetIndex, 1, bindlessBufferImages.GetSets(), 0, ReadOnlySpan<uint>.Empty);
+        }
+
+        protected virtual void Dispose(bool disposing)
+        {
+            if (disposing)
+            {
+                _idMapBuffer?.Dispose();
+                _textureScalesBuffer?.Dispose();
+            }
+        }
+
+        public void Dispose()
+        {
+            Dispose(true);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Ryujinx.Graphics.Vulkan/DescriptorSetCollection.cs b/src/Ryujinx.Graphics.Vulkan/DescriptorSetCollection.cs
index 846dd5c7d..832d7607b 100644
--- a/src/Ryujinx.Graphics.Vulkan/DescriptorSetCollection.cs
+++ b/src/Ryujinx.Graphics.Vulkan/DescriptorSetCollection.cs
@@ -88,6 +88,22 @@ namespace Ryujinx.Graphics.Vulkan
             }
         }
 
+        public unsafe void UpdateImage(int setIndex, int bindingIndex, int elementIndex, DescriptorImageInfo imageInfo, DescriptorType type)
+        {
+            var writeDescriptorSet = new WriteDescriptorSet
+            {
+                SType = StructureType.WriteDescriptorSet,
+                DstSet = _descriptorSets[setIndex],
+                DstBinding = (uint)bindingIndex,
+                DstArrayElement = (uint)elementIndex,
+                DescriptorType = type,
+                DescriptorCount = 1,
+                PImageInfo = &imageInfo
+            };
+
+            _holder.Api.UpdateDescriptorSets(_holder.Device, 1, writeDescriptorSet, 0, null);
+        }
+
         public unsafe void UpdateImages(int setIndex, int baseBinding, ReadOnlySpan<DescriptorImageInfo> imageInfo, DescriptorType type)
         {
             if (imageInfo.Length == 0)
@@ -152,7 +168,7 @@ namespace Ryujinx.Graphics.Vulkan
             }
         }
 
-        public unsafe void UpdateBufferImage(int setIndex, int bindingIndex, BufferView texelBufferView, DescriptorType type)
+        public unsafe void UpdateBufferImage(int setIndex, int bindingIndex, int elementIndex, BufferView texelBufferView, DescriptorType type)
         {
             if (texelBufferView.Handle != 0UL)
             {
@@ -161,6 +177,7 @@ namespace Ryujinx.Graphics.Vulkan
                     SType = StructureType.WriteDescriptorSet,
                     DstSet = _descriptorSets[setIndex],
                     DstBinding = (uint)bindingIndex,
+                    DstArrayElement = (uint)elementIndex,
                     DescriptorType = type,
                     DescriptorCount = 1,
                     PTexelBufferView = &texelBufferView,
diff --git a/src/Ryujinx.Graphics.Vulkan/DescriptorSetUpdater.cs b/src/Ryujinx.Graphics.Vulkan/DescriptorSetUpdater.cs
index a9a92df1d..e158d6ebc 100644
--- a/src/Ryujinx.Graphics.Vulkan/DescriptorSetUpdater.cs
+++ b/src/Ryujinx.Graphics.Vulkan/DescriptorSetUpdater.cs
@@ -61,6 +61,11 @@ namespace Ryujinx.Graphics.Vulkan
 
         private bool _updateDescriptorCacheCbIndex;
 
+        private readonly BindlessManager _bindlessManager;
+
+        public uint BindlessTexturesCount => _bindlessManager.TexturesCount;
+        public uint BindlessSamplersCount => _bindlessManager.SamplersCount;
+
         [Flags]
         private enum DirtyFlags
         {
@@ -69,7 +74,8 @@ namespace Ryujinx.Graphics.Vulkan
             Storage = 1 << 1,
             Texture = 1 << 2,
             Image = 1 << 3,
-            All = Uniform | Storage | Texture | Image,
+            Bindless = 1 << 4,
+            All = Uniform | Storage | Texture | Image | Bindless,
         }
 
         private DirtyFlags _dirty;
@@ -110,6 +116,8 @@ namespace Ryujinx.Graphics.Vulkan
             _textures.AsSpan().Fill(initialImageInfo);
             _images.AsSpan().Fill(initialImageInfo);
 
+            _bindlessManager = new BindlessManager();
+
             if (gd.Capabilities.SupportsNullDescriptors)
             {
                 // If null descriptors are supported, we can pass null as the handle.
@@ -405,6 +413,20 @@ namespace Ryujinx.Graphics.Vulkan
             SignalDirty(DirtyFlags.Uniform);
         }
 
+        public void SetBindlessTexture(int textureId, ITexture texture, float textureScale)
+        {
+            _bindlessManager.SetBindlessTexture(textureId, texture, textureScale);
+
+            SignalDirty(DirtyFlags.Bindless);
+        }
+
+        public void SetBindlessSampler(int samplerId, ISampler sampler)
+        {
+            _bindlessManager.SetBindlessSampler(samplerId, ((SamplerHolder)sampler)?.GetSampler());
+
+            SignalDirty(DirtyFlags.Bindless);
+        }
+
         private void SignalDirty(DirtyFlags flag)
         {
             _dirty |= flag;
@@ -444,7 +466,21 @@ namespace Ryujinx.Graphics.Vulkan
                 UpdateAndBind(cbs, PipelineBase.ImageSetIndex, pbp);
             }
 
-            _dirty = DirtyFlags.None;
+            DirtyFlags newFlags = DirtyFlags.None;
+
+            if (_dirty.HasFlag(DirtyFlags.Bindless))
+            {
+                if (_program.HasBindless)
+                {
+                    _bindlessManager.UpdateAndBind(_gd, _program, cbs, pbp, _dummySampler);
+                }
+                else
+                {
+                    newFlags = DirtyFlags.Bindless;
+                }
+            }
+
+            _dirty = newFlags;
         }
 
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -623,9 +659,12 @@ namespace Ryujinx.Graphics.Vulkan
                 }
             }
 
+            (uint bindlessTexturesCount, uint bindlessSamplersCount) = GetBindlessCountsForProgram();
+
+            var pipelineLayout = _program.GetPipelineLayout(_gd, bindlessTexturesCount, bindlessSamplersCount);
             var sets = dsc.GetSets();
 
-            _gd.Api.CmdBindDescriptorSets(cbs.CommandBuffer, pbp, _program.PipelineLayout, (uint)setIndex, 1, sets, 0, ReadOnlySpan<uint>.Empty);
+            _gd.Api.CmdBindDescriptorSets(cbs.CommandBuffer, pbp, pipelineLayout, (uint)setIndex, 1, sets, 0, ReadOnlySpan<uint>.Empty);
         }
 
         private unsafe void UpdateBuffers(
@@ -640,6 +679,10 @@ namespace Ryujinx.Graphics.Vulkan
                 return;
             }
 
+            (uint bindlessTexturesCount, uint bindlessSamplersCount) = GetBindlessCountsForProgram();
+
+            var pipelineLayout = _program.GetPipelineLayout(_gd, bindlessTexturesCount, bindlessSamplersCount);
+
             fixed (DescriptorBufferInfo* pBufferInfo = bufferInfo)
             {
                 var writeDescriptorSet = new WriteDescriptorSet
@@ -651,7 +694,7 @@ namespace Ryujinx.Graphics.Vulkan
                     PBufferInfo = pBufferInfo,
                 };
 
-                _gd.PushDescriptorApi.CmdPushDescriptorSet(cbs.CommandBuffer, pbp, _program.PipelineLayout, 0, 1, &writeDescriptorSet);
+                _gd.PushDescriptorApi.CmdPushDescriptorSet(cbs.CommandBuffer, pbp, pipelineLayout, 0, 1, &writeDescriptorSet);
             }
         }
 
@@ -688,6 +731,16 @@ namespace Ryujinx.Graphics.Vulkan
             }
         }
 
+        private (uint, uint) GetBindlessCountsForProgram()
+        {
+            if (_program == null || !_program.HasBindless)
+            {
+                return (0, 0);
+            }
+
+            return (BindlessTexturesCount, BindlessSamplersCount);
+        }
+
         private void Initialize(CommandBufferScoped cbs, int setIndex, DescriptorSetCollection dsc)
         {
             // We don't support clearing texture descriptors currently.
@@ -736,6 +789,8 @@ namespace Ryujinx.Graphics.Vulkan
             {
                 _dummyTexture.Dispose();
                 _dummySampler.Dispose();
+
+                _bindlessManager.Dispose();
             }
         }
 
diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs b/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs
index 7346d7891..8535b5158 100644
--- a/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs
+++ b/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs
@@ -19,11 +19,17 @@ namespace Ryujinx.Graphics.Vulkan
     class PipelineBase : IDisposable
     {
         public const int DescriptorSetLayouts = 4;
+        public const int DescriptorSetLayoutsBindless = 9;
 
         public const int UniformSetIndex = 0;
         public const int StorageSetIndex = 1;
         public const int TextureSetIndex = 2;
         public const int ImageSetIndex = 3;
+        public const int BindlessTexturesSetIndex = 4;
+        public const int BindlessBufferTextureSetIndex = 5;
+        public const int BindlessSamplersSetIndex = 6;
+        public const int BindlessImagesSetIndex = 7;
+        public const int BindlessBufferImageSetIndex = 8;
 
         protected readonly VulkanRenderer Gd;
         protected readonly Device Device;
@@ -714,6 +720,42 @@ namespace Ryujinx.Graphics.Vulkan
             _tfEnabled = false;
         }
 
+        public void RegisterBindlessSampler(int samplerId, ISampler sampler)
+        {
+            _descriptorSetUpdater.SetBindlessSampler(samplerId, sampler);
+
+            bool hasBindless = _program?.HasBindless ?? false;
+            uint bindlessSamplersCount = hasBindless ? _descriptorSetUpdater.BindlessSamplersCount : 0;
+
+            if (_newState.BindlessSamplersCount != bindlessSamplersCount)
+            {
+                _newState.BindlessSamplersCount = bindlessSamplersCount;
+
+                SignalStateChange();
+            }
+        }
+
+        public void RegisterBindlessTexture(int textureId, ITexture texture, float textureScale)
+        {
+            _descriptorSetUpdater.SetBindlessTexture(textureId, texture, textureScale);
+
+            bool hasBindless = _program?.HasBindless ?? false;
+            uint bindlessTexturesCount = hasBindless ? _descriptorSetUpdater.BindlessTexturesCount : 0;
+
+            if (_newState.BindlessTexturesCount != bindlessTexturesCount)
+            {
+                _newState.BindlessTexturesCount = bindlessTexturesCount;
+
+                SignalStateChange();
+            }
+        }
+
+        public void RegisterBindlessTextureAndSampler(int textureId, ITexture texture, float textureScale, int samplerId, ISampler sampler)
+        {
+            RegisterBindlessSampler(samplerId, sampler);
+            RegisterBindlessTexture(textureId, texture, textureScale);
+        }
+
         public bool IsCommandBufferActive(CommandBuffer cb)
         {
             return CommandBuffer.Handle == cb.Handle;
@@ -967,11 +1009,24 @@ namespace Ryujinx.Graphics.Vulkan
 
             _descriptorSetUpdater.SetProgram(internalProgram);
 
-            _newState.PipelineLayout = internalProgram.PipelineLayout;
             _newState.StagesCount = (uint)stages.Length;
 
             stages.CopyTo(_newState.Stages.AsSpan()[..stages.Length]);
 
+            if (internalProgram.HasBindless)
+            {
+                uint bindlessTexturesCount = _descriptorSetUpdater.BindlessTexturesCount;
+                uint bindlessSamplersCount = _descriptorSetUpdater.BindlessSamplersCount;
+
+                _newState.BindlessTexturesCount = bindlessTexturesCount;
+                _newState.BindlessSamplersCount = bindlessSamplersCount;
+            }
+            else
+            {
+                _newState.BindlessTexturesCount = 0;
+                _newState.BindlessSamplersCount = 0;
+            }
+
             SignalStateChange();
 
             if (internalProgram.IsCompute)
diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineLayoutCache.cs b/src/Ryujinx.Graphics.Vulkan/PipelineLayoutCache.cs
index 5d0cada96..bbc01afad 100644
--- a/src/Ryujinx.Graphics.Vulkan/PipelineLayoutCache.cs
+++ b/src/Ryujinx.Graphics.Vulkan/PipelineLayoutCache.cs
@@ -11,12 +11,12 @@ namespace Ryujinx.Graphics.Vulkan
         private readonly struct PlceKey : IEquatable<PlceKey>
         {
             public readonly ReadOnlyCollection<ResourceDescriptorCollection> SetDescriptors;
-            public readonly bool UsePushDescriptors;
+            public readonly PipelineLayoutUsageInfo UsageInfo;
 
-            public PlceKey(ReadOnlyCollection<ResourceDescriptorCollection> setDescriptors, bool usePushDescriptors)
+            public PlceKey(ReadOnlyCollection<ResourceDescriptorCollection> setDescriptors, PipelineLayoutUsageInfo usageInfo)
             {
                 SetDescriptors = setDescriptors;
-                UsePushDescriptors = usePushDescriptors;
+                UsageInfo = usageInfo;
             }
 
             public override int GetHashCode()
@@ -31,7 +31,7 @@ namespace Ryujinx.Graphics.Vulkan
                     }
                 }
 
-                hasher.Add(UsePushDescriptors);
+                hasher.Add(UsageInfo);
 
                 return hasher.ToHashCode();
             }
@@ -64,7 +64,7 @@ namespace Ryujinx.Graphics.Vulkan
                     }
                 }
 
-                return UsePushDescriptors == other.UsePushDescriptors;
+                return UsageInfo.Equals(other.UsageInfo);
             }
         }
 
@@ -72,18 +72,18 @@ namespace Ryujinx.Graphics.Vulkan
 
         public PipelineLayoutCache()
         {
-            _plces = new ConcurrentDictionary<PlceKey, PipelineLayoutCacheEntry>();
+            _plces = new();
         }
 
         public PipelineLayoutCacheEntry GetOrCreate(
             VulkanRenderer gd,
             Device device,
             ReadOnlyCollection<ResourceDescriptorCollection> setDescriptors,
-            bool usePushDescriptors)
+            PipelineLayoutUsageInfo usageInfo)
         {
-            var key = new PlceKey(setDescriptors, usePushDescriptors);
+            var key = new PlceKey(setDescriptors, usageInfo);
 
-            return _plces.GetOrAdd(key, newKey => new PipelineLayoutCacheEntry(gd, device, setDescriptors, usePushDescriptors));
+            return _plces.GetOrAdd(key, newKey => new PipelineLayoutCacheEntry(gd, device, setDescriptors, usageInfo));
         }
 
         protected virtual void Dispose(bool disposing)
diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineLayoutCacheEntry.cs b/src/Ryujinx.Graphics.Vulkan/PipelineLayoutCacheEntry.cs
index 2840dda0f..f21d40bfb 100644
--- a/src/Ryujinx.Graphics.Vulkan/PipelineLayoutCacheEntry.cs
+++ b/src/Ryujinx.Graphics.Vulkan/PipelineLayoutCacheEntry.cs
@@ -15,7 +15,7 @@ namespace Ryujinx.Graphics.Vulkan
         private const uint DefaultTexturePoolCapacity = 128 * DescriptorSetManager.MaxSets;
         private const uint DefaultImagePoolCapacity = 8 * DescriptorSetManager.MaxSets;
 
-        private const int MaxPoolSizesPerSet = 2;
+        private const int MaxPoolSizesPerSet = 3;
 
         private readonly VulkanRenderer _gd;
         private readonly Device _device;
@@ -31,6 +31,9 @@ namespace Ryujinx.Graphics.Vulkan
         private int _dsLastCbIndex;
         private int _dsLastSubmissionCount;
 
+        private readonly uint _bindlessTexturesCount;
+        private readonly uint _bindlessSamplersCount;
+
         private PipelineLayoutCacheEntry(VulkanRenderer gd, Device device, int setsCount)
         {
             _gd = gd;
@@ -44,7 +47,7 @@ namespace Ryujinx.Graphics.Vulkan
 
                 for (int j = 0; j < _dsCache[i].Length; j++)
                 {
-                    _dsCache[i][j] = new List<Auto<DescriptorSetCollection>>();
+                    _dsCache[i][j] = new();
                 }
             }
 
@@ -55,9 +58,9 @@ namespace Ryujinx.Graphics.Vulkan
             VulkanRenderer gd,
             Device device,
             ReadOnlyCollection<ResourceDescriptorCollection> setDescriptors,
-            bool usePushDescriptors) : this(gd, device, setDescriptors.Count)
+            PipelineLayoutUsageInfo usageInfo) : this(gd, device, setDescriptors.Count)
         {
-            (DescriptorSetLayouts, PipelineLayout) = PipelineLayoutFactory.Create(gd, device, setDescriptors, usePushDescriptors);
+            (DescriptorSetLayouts, PipelineLayout) = PipelineLayoutFactory.Create(gd, device, setDescriptors, usageInfo);
 
             _consumedDescriptorsPerSet = new int[setDescriptors.Count];
 
@@ -72,6 +75,9 @@ namespace Ryujinx.Graphics.Vulkan
 
                 _consumedDescriptorsPerSet[setIndex] = count;
             }
+
+            _bindlessSamplersCount = usageInfo.BindlessSamplersCount;
+            _bindlessTexturesCount = usageInfo.BindlessTexturesCount;
         }
 
         public void UpdateCommandBufferIndex(int commandBufferIndex)
@@ -99,13 +105,18 @@ namespace Ryujinx.Graphics.Vulkan
 
                 int consumedDescriptors = _consumedDescriptorsPerSet[setIndex];
 
+                // All bindless resources have the update after bind flag set,
+                // this is required for Intel, otherwise it just crashes when binding the descriptor sets
+                // for bindless resources (maybe because it is above the limit?)
+                bool updateAfterBind = setIndex >= PipelineBase.BindlessBufferTextureSetIndex;
+
                 var dsc = _gd.DescriptorSetManager.AllocateDescriptorSet(
                     _gd.Api,
                     DescriptorSetLayouts[setIndex],
                     poolSizes,
                     setIndex,
                     consumedDescriptors,
-                    false);
+                    updateAfterBind);
 
                 list.Add(dsc);
                 isNew = true;
@@ -116,7 +127,7 @@ namespace Ryujinx.Graphics.Vulkan
             return list[index];
         }
 
-        private static Span<DescriptorPoolSize> GetDescriptorPoolSizes(Span<DescriptorPoolSize> output, int setIndex)
+        private Span<DescriptorPoolSize> GetDescriptorPoolSizes(Span<DescriptorPoolSize> output, int setIndex)
         {
             int count = 1;
 
@@ -138,6 +149,24 @@ namespace Ryujinx.Graphics.Vulkan
                     output[1] = new(DescriptorType.StorageTexelBuffer, DefaultImagePoolCapacity);
                     count = 2;
                     break;
+                case PipelineBase.BindlessTexturesSetIndex:
+                    output[0] = new(DescriptorType.UniformBuffer, 1);
+                    output[1] = new(DescriptorType.StorageBuffer, 1);
+                    output[2] = new(DescriptorType.SampledImage, _bindlessTexturesCount);
+                    count = 3;
+                    break;
+                case PipelineBase.BindlessBufferTextureSetIndex:
+                    output[0] = new(DescriptorType.UniformTexelBuffer, _bindlessTexturesCount);
+                    break;
+                case PipelineBase.BindlessSamplersSetIndex:
+                    output[0] = new(DescriptorType.Sampler, _bindlessSamplersCount);
+                    break;
+                case PipelineBase.BindlessImagesSetIndex:
+                    output[0] = new(DescriptorType.StorageImage, _bindlessTexturesCount);
+                    break;
+                case PipelineBase.BindlessBufferImageSetIndex:
+                    output[0] = new(DescriptorType.StorageTexelBuffer, _bindlessTexturesCount);
+                    break;
             }
 
             return output[..count];
diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineLayoutFactory.cs b/src/Ryujinx.Graphics.Vulkan/PipelineLayoutFactory.cs
index ba93dfadb..9e667fe7c 100644
--- a/src/Ryujinx.Graphics.Vulkan/PipelineLayoutFactory.cs
+++ b/src/Ryujinx.Graphics.Vulkan/PipelineLayoutFactory.cs
@@ -10,7 +10,7 @@ namespace Ryujinx.Graphics.Vulkan
             VulkanRenderer gd,
             Device device,
             ReadOnlyCollection<ResourceDescriptorCollection> setDescriptors,
-            bool usePushDescriptors)
+            PipelineLayoutUsageInfo usageInfo)
         {
             DescriptorSetLayout[] layouts = new DescriptorSetLayout[setDescriptors.Count];
 
@@ -30,6 +30,8 @@ namespace Ryujinx.Graphics.Vulkan
                     }
                 }
 
+                bool hasRuntimeArray = false;
+
                 DescriptorSetLayoutBinding[] layoutBindings = new DescriptorSetLayoutBinding[rdc.Descriptors.Count];
 
                 for (int descIndex = 0; descIndex < rdc.Descriptors.Count; descIndex++)
@@ -45,11 +47,22 @@ namespace Ryujinx.Graphics.Vulkan
                         stages = activeStages;
                     }
 
+                    uint count = (uint)descriptor.Count;
+
+                    if (count == 0)
+                    {
+                        count = descriptor.Type == ResourceType.Sampler
+                            ? usageInfo.BindlessSamplersCount
+                            : usageInfo.BindlessTexturesCount;
+
+                        hasRuntimeArray = true;
+                    }
+
                     layoutBindings[descIndex] = new DescriptorSetLayoutBinding
                     {
                         Binding = (uint)descriptor.Binding,
                         DescriptorType = descriptor.Type.Convert(),
-                        DescriptorCount = (uint)descriptor.Count,
+                        DescriptorCount = count,
                         StageFlags = stages.Convert(),
                     };
                 }
@@ -61,10 +74,40 @@ namespace Ryujinx.Graphics.Vulkan
                         SType = StructureType.DescriptorSetLayoutCreateInfo,
                         PBindings = pLayoutBindings,
                         BindingCount = (uint)layoutBindings.Length,
-                        Flags = usePushDescriptors && setIndex == 0 ? DescriptorSetLayoutCreateFlags.PushDescriptorBitKhr : DescriptorSetLayoutCreateFlags.None,
+                        Flags = usageInfo.UsePushDescriptors && setIndex == 0 ? DescriptorSetLayoutCreateFlags.PushDescriptorBitKhr : DescriptorSetLayoutCreateFlags.None,
                     };
 
-                    gd.Api.CreateDescriptorSetLayout(device, descriptorSetLayoutCreateInfo, null, out layouts[setIndex]).ThrowOnError();
+                    if (hasRuntimeArray)
+                    {
+                        var bindingFlags = new DescriptorBindingFlags[rdc.Descriptors.Count];
+
+                        for (int descIndex = 0; descIndex < rdc.Descriptors.Count; descIndex++)
+                        {
+                            if (rdc.Descriptors.Count == 0)
+                            {
+                                bindingFlags[descIndex] = DescriptorBindingFlags.UpdateAfterBindBit;
+                            }
+                        }
+
+                        fixed (DescriptorBindingFlags* pBindingFlags = bindingFlags)
+                        {
+                            var descriptorSetLayoutFlagsCreateInfo = new DescriptorSetLayoutBindingFlagsCreateInfo()
+                            {
+                                SType = StructureType.DescriptorSetLayoutBindingFlagsCreateInfo,
+                                PBindingFlags = pBindingFlags,
+                                BindingCount = (uint)bindingFlags.Length,
+                            };
+
+                            descriptorSetLayoutCreateInfo.PNext = &descriptorSetLayoutFlagsCreateInfo;
+                            descriptorSetLayoutCreateInfo.Flags |= DescriptorSetLayoutCreateFlags.UpdateAfterBindPoolBit;
+
+                            gd.Api.CreateDescriptorSetLayout(device, descriptorSetLayoutCreateInfo, null, out layouts[setIndex]).ThrowOnError();
+                        }
+                    }
+                    else
+                    {
+                        gd.Api.CreateDescriptorSetLayout(device, descriptorSetLayoutCreateInfo, null, out layouts[setIndex]).ThrowOnError();
+                    }
                 }
             }
 
diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineLayoutUsageInfo.cs b/src/Ryujinx.Graphics.Vulkan/PipelineLayoutUsageInfo.cs
new file mode 100644
index 000000000..2174c2b9d
--- /dev/null
+++ b/src/Ryujinx.Graphics.Vulkan/PipelineLayoutUsageInfo.cs
@@ -0,0 +1,35 @@
+using System;
+
+namespace Ryujinx.Graphics.Vulkan
+{
+    struct PipelineLayoutUsageInfo : IEquatable<PipelineLayoutUsageInfo>
+    {
+        public readonly uint BindlessTexturesCount;
+        public readonly uint BindlessSamplersCount;
+        public readonly bool UsePushDescriptors;
+
+        public PipelineLayoutUsageInfo(uint bindlessTexturesCount, uint bindlessSamplersCount, bool usePushDescriptors)
+        {
+            BindlessTexturesCount = bindlessTexturesCount;
+            BindlessSamplersCount = bindlessSamplersCount;
+            UsePushDescriptors = usePushDescriptors;
+        }
+
+        public override bool Equals(object obj)
+        {
+            return obj is PipelineLayoutUsageInfo other && Equals(other);
+        }
+
+        public bool Equals(PipelineLayoutUsageInfo other)
+        {
+            return BindlessTexturesCount == other.BindlessTexturesCount &&
+                   BindlessSamplersCount == other.BindlessSamplersCount &&
+                   UsePushDescriptors == other.UsePushDescriptors;
+        }
+
+        public override int GetHashCode()
+        {
+            return HashCode.Combine(BindlessTexturesCount, BindlessSamplersCount, UsePushDescriptors);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineState.cs b/src/Ryujinx.Graphics.Vulkan/PipelineState.cs
index 5a30cff8e..90893e839 100644
--- a/src/Ryujinx.Graphics.Vulkan/PipelineState.cs
+++ b/src/Ryujinx.Graphics.Vulkan/PipelineState.cs
@@ -311,9 +311,20 @@ namespace Ryujinx.Graphics.Vulkan
             set => Internal.Id9 = (Internal.Id9 & 0xFFFFFFFFFFFFFFBF) | ((value ? 1UL : 0UL) << 6);
         }
 
+        public uint BindlessTexturesCount
+        {
+            get => (uint)((Internal.Id10 >> 0) & 0xFFFFFFFF);
+            set => Internal.Id10 = (Internal.Id10 & 0xFFFFFFFF00000000) | ((ulong)value << 0);
+        }
+
+        public uint BindlessSamplersCount
+        {
+            get => (uint)((Internal.Id10 >> 32) & 0xFFFFFFFF);
+            set => Internal.Id10 = (Internal.Id10 & 0xFFFFFFFF) | ((ulong)value << 32);
+        }
+
         public NativeArray<PipelineShaderStageCreateInfo> Stages;
         public NativeArray<PipelineShaderStageRequiredSubgroupSizeCreateInfoEXT> StageRequiredSubgroupSizes;
-        public PipelineLayout PipelineLayout;
         public SpecData SpecializationData;
 
         private Array32<VertexInputAttributeDescription> _vertexAttributeDescriptions2;
@@ -339,6 +350,7 @@ namespace Ryujinx.Graphics.Vulkan
             LineWidth = 1f;
             SamplesCount = 1;
             DepthMode = true;
+            Internal.Id11 = 0; // Unused.
         }
 
         public unsafe Auto<DisposablePipeline> CreateComputePipeline(
@@ -357,7 +369,7 @@ namespace Ryujinx.Graphics.Vulkan
                 SType = StructureType.ComputePipelineCreateInfo,
                 Stage = Stages[0],
                 BasePipelineIndex = -1,
-                Layout = PipelineLayout,
+                Layout = program.GetPipelineLayout(gd, BindlessTexturesCount, BindlessSamplersCount),
             };
 
             Pipeline pipelineHandle = default;
@@ -625,7 +637,7 @@ namespace Ryujinx.Graphics.Vulkan
                     PDepthStencilState = &depthStencilState,
                     PColorBlendState = &colorBlendState,
                     PDynamicState = &pipelineDynamicStateCreateInfo,
-                    Layout = PipelineLayout,
+                    Layout = program.GetPipelineLayout(gd, BindlessTexturesCount, BindlessSamplersCount),
                     RenderPass = renderPass,
                     BasePipelineIndex = -1,
                 };
diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineUid.cs b/src/Ryujinx.Graphics.Vulkan/PipelineUid.cs
index 460c27d8b..6c7edd569 100644
--- a/src/Ryujinx.Graphics.Vulkan/PipelineUid.cs
+++ b/src/Ryujinx.Graphics.Vulkan/PipelineUid.cs
@@ -21,6 +21,8 @@ namespace Ryujinx.Graphics.Vulkan
 
         public ulong Id8;
         public ulong Id9;
+        public ulong Id10;
+        public ulong Id11;
 
         private readonly uint VertexAttributeDescriptionsCount => (byte)((Id6 >> 38) & 0xFF);
         private readonly uint VertexBindingDescriptionsCount => (byte)((Id6 >> 46) & 0xFF);
@@ -44,7 +46,7 @@ namespace Ryujinx.Graphics.Vulkan
         {
             if (!Unsafe.As<ulong, Vector256<byte>>(ref Id0).Equals(Unsafe.As<ulong, Vector256<byte>>(ref other.Id0)) ||
                 !Unsafe.As<ulong, Vector256<byte>>(ref Id4).Equals(Unsafe.As<ulong, Vector256<byte>>(ref other.Id4)) ||
-                !Unsafe.As<ulong, Vector128<byte>>(ref Id8).Equals(Unsafe.As<ulong, Vector128<byte>>(ref other.Id8)))
+                !Unsafe.As<ulong, Vector256<byte>>(ref Id8).Equals(Unsafe.As<ulong, Vector256<byte>>(ref other.Id8)))
             {
                 return false;
             }
@@ -88,7 +90,8 @@ namespace Ryujinx.Graphics.Vulkan
                            Id6 * 23 ^
                            Id7 * 23 ^
                            Id8 * 23 ^
-                           Id9 * 23;
+                           Id9 * 23 ^
+                           Id10 * 23;
 
             for (int i = 0; i < (int)VertexAttributeDescriptionsCount; i++)
             {
diff --git a/src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs b/src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs
index 0cb80ac71..2a01ec05d 100644
--- a/src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs
+++ b/src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs
@@ -15,9 +15,9 @@ namespace Ryujinx.Graphics.Vulkan
         private readonly Shader[] _shaders;
 
         private readonly PipelineLayoutCacheEntry _plce;
+        private readonly ReadOnlyCollection<ResourceDescriptorCollection> _setDescriptors;
 
-        public PipelineLayout PipelineLayout => _plce.PipelineLayout;
-
+        public bool HasBindless { get; }
         public bool HasMinimalLayout { get; }
         public bool UsePushDescriptors { get; }
         public bool IsCompute { get; }
@@ -62,6 +62,7 @@ namespace Ryujinx.Graphics.Vulkan
             ShaderSource[] shaders,
             ResourceLayout resourceLayout,
             SpecDescription[] specDescription = null,
+            bool hasBindless = false,
             bool isMinimal = false)
         {
             _gd = gd;
@@ -109,8 +110,10 @@ namespace Ryujinx.Graphics.Vulkan
 
             bool usePushDescriptors = !isMinimal && VulkanConfiguration.UsePushDescriptors && _gd.Capabilities.SupportsPushDescriptors;
 
-            _plce = gd.PipelineLayoutCache.GetOrCreate(gd, device, resourceLayout.Sets, usePushDescriptors);
+            _plce = gd.PipelineLayoutCache.GetOrCreate(gd, device, resourceLayout.Sets, new PipelineLayoutUsageInfo(0, 0, usePushDescriptors));
+            _setDescriptors = resourceLayout.Sets;
 
+            HasBindless = hasBindless;
             HasMinimalLayout = isMinimal;
             UsePushDescriptors = usePushDescriptors;
 
@@ -129,7 +132,8 @@ namespace Ryujinx.Graphics.Vulkan
             ShaderSource[] sources,
             ResourceLayout resourceLayout,
             ProgramPipelineState state,
-            bool fromCache) : this(gd, device, sources, resourceLayout)
+            bool hasBindless,
+            bool fromCache) : this(gd, device, sources, resourceLayout, null, hasBindless)
         {
             _state = state;
 
@@ -325,7 +329,12 @@ namespace Ryujinx.Graphics.Vulkan
 
             pipeline.Stages[0] = _shaders[0].GetInfo();
             pipeline.StagesCount = 1;
-            pipeline.PipelineLayout = PipelineLayout;
+
+            if (HasBindless)
+            {
+                pipeline.BindlessTexturesCount = BindlessManager.MinimumTexturesCount;
+                pipeline.BindlessSamplersCount = BindlessManager.MinimumSamplersCount;
+            }
 
             pipeline.CreateComputePipeline(_gd, _device, this, (_gd.Pipeline as PipelineBase).PipelineCache);
             pipeline.Dispose();
@@ -353,7 +362,12 @@ namespace Ryujinx.Graphics.Vulkan
             }
 
             pipeline.StagesCount = (uint)_shaders.Length;
-            pipeline.PipelineLayout = PipelineLayout;
+
+            if (HasBindless)
+            {
+                pipeline.BindlessTexturesCount = BindlessManager.MinimumTexturesCount;
+                pipeline.BindlessSamplersCount = BindlessManager.MinimumSamplersCount;
+            }
 
             pipeline.CreateGraphicsPipeline(_gd, _device, this, (_gd.Pipeline as PipelineBase).PipelineCache, renderPass.Value);
             pipeline.Dispose();
@@ -474,6 +488,26 @@ namespace Ryujinx.Graphics.Vulkan
             return _plce.GetNewDescriptorSetCollection(setIndex, out isNew);
         }
 
+        public PipelineLayout GetPipelineLayout(VulkanRenderer gd, uint bindlessTextureCount, uint bindlessSamplersCount)
+        {
+            return GetPipelineLayoutCacheEntry(gd, bindlessTextureCount, bindlessSamplersCount).PipelineLayout;
+        }
+
+        public PipelineLayoutCacheEntry GetPipelineLayoutCacheEntry(VulkanRenderer gd, uint bindlessTextureCount, uint bindlessSamplersCount)
+        {
+            if ((bindlessTextureCount | bindlessSamplersCount) == 0)
+            {
+                return _plce;
+            }
+
+            var usageInfo = new PipelineLayoutUsageInfo(
+                bindlessTextureCount,
+                bindlessSamplersCount,
+                UsePushDescriptors);
+
+            return gd.PipelineLayoutCache.GetOrCreate(gd, _device, _setDescriptors, usageInfo);
+        }
+
         protected virtual void Dispose(bool disposing)
         {
             if (disposing)
diff --git a/src/Ryujinx.Graphics.Vulkan/TextureBuffer.cs b/src/Ryujinx.Graphics.Vulkan/TextureBuffer.cs
index 285a56498..a610bca59 100644
--- a/src/Ryujinx.Graphics.Vulkan/TextureBuffer.cs
+++ b/src/Ryujinx.Graphics.Vulkan/TextureBuffer.cs
@@ -23,6 +23,7 @@ namespace Ryujinx.Graphics.Vulkan
         public int Width { get; }
         public int Height { get; }
 
+        public GAL.Format Format { get; }
         public VkFormat VkFormat { get; }
 
         public TextureBuffer(VulkanRenderer gd, TextureCreateInfo info)
@@ -30,6 +31,7 @@ namespace Ryujinx.Graphics.Vulkan
             _gd = gd;
             Width = info.Width;
             Height = info.Height;
+            Format = info.Format;
             VkFormat = FormatTable.GetFormat(info.Format);
 
             gd.Textures.Add(this);
diff --git a/src/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs b/src/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs
index 973c6d396..546aa5f2e 100644
--- a/src/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs
+++ b/src/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs
@@ -299,6 +299,14 @@ namespace Ryujinx.Graphics.Vulkan
 
             features2.PNext = &supportedFeaturesVk11;
 
+            PhysicalDeviceVulkan12Features supportedFeaturesVk12 = new()
+            {
+                SType = StructureType.PhysicalDeviceVulkan12Features,
+                PNext = features2.PNext
+            };
+
+            features2.PNext = &supportedFeaturesVk12;
+
             PhysicalDeviceCustomBorderColorFeaturesEXT supportedFeaturesCustomBorderColor = new()
             {
                 SType = StructureType.PhysicalDeviceCustomBorderColorFeaturesExt,
@@ -451,8 +459,14 @@ namespace Ryujinx.Graphics.Vulkan
             {
                 SType = StructureType.PhysicalDeviceVulkan12Features,
                 PNext = pExtendedFeatures,
+                DescriptorBindingSampledImageUpdateAfterBind = supportedFeaturesVk12.DescriptorBindingSampledImageUpdateAfterBind,
+                DescriptorBindingStorageImageUpdateAfterBind = supportedFeaturesVk12.DescriptorBindingStorageImageUpdateAfterBind,
+                DescriptorBindingStorageTexelBufferUpdateAfterBind = supportedFeaturesVk12.DescriptorBindingStorageTexelBufferUpdateAfterBind,
+                DescriptorBindingUniformTexelBufferUpdateAfterBind = supportedFeaturesVk12.DescriptorBindingUniformTexelBufferUpdateAfterBind,
                 DescriptorIndexing = physicalDevice.IsDeviceExtensionPresent("VK_EXT_descriptor_indexing"),
                 DrawIndirectCount = physicalDevice.IsDeviceExtensionPresent(KhrDrawIndirectCount.ExtensionName),
+                RuntimeDescriptorArray = supportedFeaturesVk12.RuntimeDescriptorArray,
+                SamplerMirrorClampToEdge = supportedFeaturesVk12.SamplerMirrorClampToEdge,
                 UniformBufferStandardLayout = physicalDevice.IsDeviceExtensionPresent("VK_KHR_uniform_buffer_standard_layout"),
             };
 
diff --git a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs
index ab8e61371..84cb38b31 100644
--- a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs
+++ b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs
@@ -347,7 +347,7 @@ namespace Ryujinx.Graphics.Vulkan
 
             CommandBufferPool = new CommandBufferPool(Api, _device, Queue, QueueLock, queueFamilyIndex);
 
-            DescriptorSetManager = new DescriptorSetManager(_device, PipelineBase.DescriptorSetLayouts);
+            DescriptorSetManager = new DescriptorSetManager(_device, PipelineBase.DescriptorSetLayoutsBindless);
 
             PipelineLayoutCache = new PipelineLayoutCache();
 
@@ -418,7 +418,7 @@ namespace Ryujinx.Graphics.Vulkan
 
             if (info.State.HasValue || isCompute)
             {
-                return new ShaderCollection(this, _device, sources, info.ResourceLayout, info.State ?? default, info.FromCache);
+                return new ShaderCollection(this, _device, sources, info.ResourceLayout, info.State ?? default, info.HasBindless, info.FromCache);
             }
 
             return new ShaderCollection(this, _device, sources, info.ResourceLayout);