From 8f0c89ffd65eb2b979b24d457708218050fec6ae Mon Sep 17 00:00:00 2001
From: gdkchan <gab.dark.100@gmail.com>
Date: Thu, 25 May 2023 17:46:58 -0300
Subject: [PATCH] Generate scaling helper functions on IR (#4714)

* Generate scaling helper functions on IR

* Delete unused code

* Split RewriteTextureSample and move gather bias add to an earlier pass

* Remove using

* Shader cache version bump
---
 .../Shader/DiskCache/DiskCacheHostStorage.cs  |   2 +-
 .../CodeGen/Glsl/Declarations.cs              |  29 +-
 .../HelperFunctions/TexelFetchScale_cp.glsl   |  19 -
 .../HelperFunctions/TexelFetchScale_fp.glsl   |  26 -
 .../HelperFunctions/TexelFetchScale_vp.glsl   |  20 -
 .../Glsl/Instructions/InstGenHelper.cs        |   1 +
 .../Glsl/Instructions/InstGenMemory.cs        |  84 +---
 .../CodeGen/Spirv/CodeGenContext.cs           |   1 +
 .../CodeGen/Spirv/Instructions.cs             |  54 +--
 .../CodeGen/Spirv/ScalingHelpers.cs           | 227 ---------
 .../CodeGen/Spirv/SpirvDelegates.cs           |   2 +
 .../CodeGen/Spirv/SpirvGenerator.cs           |   5 +-
 .../IntermediateRepresentation/Instruction.cs |   1 +
 .../Ryujinx.Graphics.Shader.csproj            |   7 -
 .../StructuredIr/AstTextureOperation.cs       |   5 -
 .../StructuredIr/InstructionInfo.cs           |   1 +
 .../StructuredIr/StructuredProgram.cs         |   4 +-
 .../Translation/EmitterContext.cs             |  10 +-
 .../Translation/EmitterContextInsts.cs        |   6 +-
 .../Translation/HelperFunctionManager.cs      | 134 ++++++
 .../Translation/HelperFunctionName.cs         |  11 +
 .../Translation/Rewriter.cs                   | 445 +++++++++++++-----
 .../Translation/ShaderConfig.cs               |  30 +-
 .../Translation/ShaderIdentifier.cs           |   8 +-
 .../Translation/Translator.cs                 |  12 +-
 25 files changed, 560 insertions(+), 584 deletions(-)
 delete mode 100644 src/Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/TexelFetchScale_cp.glsl
 delete mode 100644 src/Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/TexelFetchScale_fp.glsl
 delete mode 100644 src/Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/TexelFetchScale_vp.glsl
 delete mode 100644 src/Ryujinx.Graphics.Shader/CodeGen/Spirv/ScalingHelpers.cs
 create mode 100644 src/Ryujinx.Graphics.Shader/Translation/HelperFunctionManager.cs
 create mode 100644 src/Ryujinx.Graphics.Shader/Translation/HelperFunctionName.cs

diff --git a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs
index 400f63f567..b8d74a463f 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 = 5031;
+        private const uint CodeGenVersion = 4714;
 
         private const string SharedTocFileName = "shared.toc";
         private const string SharedDataFileName = "shared.data";
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Declarations.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Declarations.cs
index 8d805e32ec..1bd0182b5a 100644
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Declarations.cs
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Declarations.cs
@@ -239,33 +239,10 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
                 context.AppendLine();
             }
 
-            bool isFragment = context.Config.Stage == ShaderStage.Fragment;
-
-            if (isFragment || context.Config.Stage == ShaderStage.Compute || context.Config.Stage == ShaderStage.Vertex)
+            if (context.Config.Stage == ShaderStage.Fragment && context.Config.GpuAccessor.QueryEarlyZForce())
             {
-                if (isFragment && context.Config.GpuAccessor.QueryEarlyZForce())
-                {
-                    context.AppendLine("layout(early_fragment_tests) in;");
-                    context.AppendLine();
-                }
-
-                if ((context.Config.UsedFeatures & (FeatureFlags.FragCoordXY | FeatureFlags.IntegerSampling)) != 0)
-                {
-                    string stage = OperandManager.GetShaderStagePrefix(context.Config.Stage);
-
-                    int scaleElements = context.Config.GetTextureDescriptors().Length + context.Config.GetImageDescriptors().Length;
-
-                    if (isFragment)
-                    {
-                        scaleElements++; // Also includes render target scale, for gl_FragCoord.
-                    }
-
-                    if (context.Config.UsedFeatures.HasFlag(FeatureFlags.IntegerSampling) && scaleElements != 0)
-                    {
-                        AppendHelperFunction(context, $"Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/TexelFetchScale_{stage}.glsl");
-                        context.AppendLine();
-                    }
-                }
+                context.AppendLine("layout(early_fragment_tests) in;");
+                context.AppendLine();
             }
 
             if ((info.HelperFunctionsMask & HelperFunctionsMask.AtomicMinMaxS32Shared) != 0)
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/TexelFetchScale_cp.glsl b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/TexelFetchScale_cp.glsl
deleted file mode 100644
index 08c6254882..0000000000
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/TexelFetchScale_cp.glsl
+++ /dev/null
@@ -1,19 +0,0 @@
-ivec2 Helper_TexelFetchScale(ivec2 inputVec, int samplerIndex)
-{
-    float scale = support_buffer.s_render_scale[1 + samplerIndex];
-    if (scale == 1.0)
-    {
-        return inputVec;
-    }
-    return ivec2(vec2(inputVec) * scale);
-}
-
-int Helper_TextureSizeUnscale(int size, int samplerIndex)
-{
-    float scale = support_buffer.s_render_scale[1 + samplerIndex];
-    if (scale == 1.0)
-    {
-        return size;
-    }
-    return int(float(size) / scale);
-}
\ No newline at end of file
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/TexelFetchScale_fp.glsl b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/TexelFetchScale_fp.glsl
deleted file mode 100644
index 07a38a7a68..0000000000
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/TexelFetchScale_fp.glsl
+++ /dev/null
@@ -1,26 +0,0 @@
-ivec2 Helper_TexelFetchScale(ivec2 inputVec, int samplerIndex)
-{
-    float scale = support_buffer.s_render_scale[1 + samplerIndex];
-    if (scale == 1.0)
-    {
-        return inputVec;
-    }
-    if (scale < 0.0) // If less than 0, try interpolate between texels by using the screen position.
-    {
-        return ivec2(vec2(inputVec) * (-scale) + mod(gl_FragCoord.xy, 0.0 - scale));
-    }
-    else
-    {
-        return ivec2(vec2(inputVec) * scale);
-    }
-}
-
-int Helper_TextureSizeUnscale(int size, int samplerIndex)
-{
-    float scale = abs(support_buffer.s_render_scale[1 + samplerIndex]);
-    if (scale == 1.0)
-    {
-        return size;
-    }
-    return int(float(size) / scale);
-}
\ No newline at end of file
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/TexelFetchScale_vp.glsl b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/TexelFetchScale_vp.glsl
deleted file mode 100644
index 72baa441d6..0000000000
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/HelperFunctions/TexelFetchScale_vp.glsl
+++ /dev/null
@@ -1,20 +0,0 @@
-ivec2 Helper_TexelFetchScale(ivec2 inputVec, int samplerIndex)
-{
-    float scale = abs(support_buffer.s_render_scale[1 + samplerIndex + support_buffer.s_frag_scale_count]);
-    if (scale == 1.0)
-    {
-        return inputVec;
-    }
-
-    return ivec2(vec2(inputVec) * scale);
-}
-
-int Helper_TextureSizeUnscale(int size, int samplerIndex)
-{
-    float scale = abs(support_buffer.s_render_scale[1 + samplerIndex + support_buffer.s_frag_scale_count]);
-    if (scale == 1.0)
-    {
-        return size;
-    }
-    return int(float(size) / scale);
-}
\ No newline at end of file
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenHelper.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenHelper.cs
index 71e40fe7c6..6cf36a2a6d 100644
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenHelper.cs
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenHelper.cs
@@ -101,6 +101,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
             Add(Instruction.MemoryBarrier,            InstType.CallNullary,    "memoryBarrier");
             Add(Instruction.Minimum,                  InstType.CallBinary,     "min");
             Add(Instruction.MinimumU32,               InstType.CallBinary,     "min");
+            Add(Instruction.Modulo,                   InstType.CallBinary,     "mod");
             Add(Instruction.Multiply,                 InstType.OpBinaryCom,    "*",               1);
             Add(Instruction.MultiplyHighS32,          InstType.CallBinary,     HelperFunctionNames.MultiplyHighS32);
             Add(Instruction.MultiplyHighU32,          InstType.CallBinary,     HelperFunctionNames.MultiplyHighU32);
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenMemory.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenMemory.cs
index ef5260d108..dfc8197b64 100644
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenMemory.cs
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenMemory.cs
@@ -97,30 +97,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
                 texCallBuilder.Append(str);
             }
 
-            string ApplyScaling(string vector)
-            {
-                if (context.Config.Stage.SupportsRenderScale() &&
-                    texOp.Inst == Instruction.ImageLoad &&
-                    !isBindless &&
-                    !isIndexed)
-                {
-                    // Image scales start after texture ones.
-                    int scaleIndex = context.Config.GetTextureDescriptors().Length + context.Config.FindImageDescriptorIndex(texOp);
-
-                    if (pCount == 3 && isArray)
-                    {
-                        // The array index is not scaled, just x and y.
-                        vector = $"ivec3(Helper_TexelFetchScale(({vector}).xy, {scaleIndex}), ({vector}).z)";
-                    }
-                    else if (pCount == 2 && !isArray)
-                    {
-                        vector = $"Helper_TexelFetchScale({vector}, {scaleIndex})";
-                    }
-                }
-
-                return vector;
-            }
-
             if (pCount > 1)
             {
                 string[] elems = new string[pCount];
@@ -130,7 +106,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
                     elems[index] = Src(AggregateType.S32);
                 }
 
-                Append(ApplyScaling($"ivec{pCount}({string.Join(", ", elems)})"));
+                Append($"ivec{pCount}({string.Join(", ", elems)})");
             }
             else
             {
@@ -584,53 +560,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
                 }
             }
 
-            string ApplyScaling(string vector)
-            {
-                if (intCoords)
-                {
-                    if (context.Config.Stage.SupportsRenderScale() &&
-                        !isBindless &&
-                        !isIndexed)
-                    {
-                        int index = context.Config.FindTextureDescriptorIndex(texOp);
-
-                        if (pCount == 3 && isArray)
-                        {
-                            // The array index is not scaled, just x and y.
-                            vector = "ivec3(Helper_TexelFetchScale((" + vector + ").xy, " + index + "), (" + vector + ").z)";
-                        }
-                        else if (pCount == 2 && !isArray)
-                        {
-                            vector = "Helper_TexelFetchScale(" + vector + ", " + index + ")";
-                        }
-                    }
-                }
-
-                return vector;
-            }
-
-            string ApplyBias(string vector)
-            {
-                int gatherBiasPrecision = context.Config.GpuAccessor.QueryHostGatherBiasPrecision();
-                if (isGather && gatherBiasPrecision != 0)
-                {
-                    // GPU requires texture gather to be slightly offset to match NVIDIA behaviour when point is exactly between two texels.
-                    // Offset by the gather precision divided by 2 to correct for rounding.
-
-                    if (pCount == 1)
-                    {
-                        vector = $"{vector} + (1.0 / (float(textureSize({samplerName}, 0)) * float({1 << (gatherBiasPrecision + 1)})))";
-                    }
-                    else
-                    {
-                        vector = $"{vector} + (1.0 / (vec{pCount}(textureSize({samplerName}, 0).{"xyz".Substring(0, pCount)}) * float({1 << (gatherBiasPrecision + 1)})))";
-                    }
-                }
-
-                return vector;
-            }
-
-            Append(ApplyBias(ApplyScaling(AssemblePVector(pCount))));
+            Append(AssemblePVector(pCount));
 
             string AssembleDerivativesVector(int count)
             {
@@ -750,7 +680,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
             }
             else
             {
-                (TextureDescriptor descriptor, int descriptorIndex) = context.Config.FindTextureDescriptor(texOp);
+                TextureDescriptor descriptor = context.Config.FindTextureDescriptor(texOp);
                 bool hasLod = !descriptor.Type.HasFlag(SamplerType.Multisample) && descriptor.Type != SamplerType.TextureBuffer;
                 string texCall;
 
@@ -767,14 +697,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
                     texCall = $"textureSize({samplerName}){GetMask(texOp.Index)}";
                 }
 
-                if (context.Config.Stage.SupportsRenderScale() &&
-                    (texOp.Index < 2 || (texOp.Type & SamplerType.Mask) == SamplerType.Texture3D) &&
-                    !isBindless &&
-                    !isIndexed)
-                {
-                    texCall = $"Helper_TextureSizeUnscale({texCall}, {descriptorIndex})";
-                }
-
                 return texCall;
             }
         }
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/CodeGenContext.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/CodeGenContext.cs
index 0ef89b398b..7af6d316ed 100644
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/CodeGenContext.cs
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/CodeGenContext.cs
@@ -36,6 +36,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
         public Dictionary<IoDefinition, Instruction> OutputsPerPatch { get; } = new Dictionary<IoDefinition, Instruction>();
 
         public Instruction CoordTemp { get; set; }
+        public StructuredFunction CurrentFunction { get; set; }
         private readonly Dictionary<AstOperand, Instruction> _locals = new Dictionary<AstOperand, Instruction>();
         private readonly Dictionary<int, Instruction[]> _localForArgs = new Dictionary<int, Instruction[]>();
         private readonly Dictionary<int, Instruction> _funcArgs = new Dictionary<int, Instruction>();
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs
index fda0dc47cc..eb64f82418 100644
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs
@@ -4,7 +4,6 @@ using Ryujinx.Graphics.Shader.Translation;
 using System;
 using System.Collections.Generic;
 using System.Diagnostics;
-using System.Linq;
 using System.Numerics;
 using static Spv.Specification;
 
@@ -114,6 +113,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
             Add(Instruction.MemoryBarrier,            GenerateMemoryBarrier);
             Add(Instruction.Minimum,                  GenerateMinimum);
             Add(Instruction.MinimumU32,               GenerateMinimumU32);
+            Add(Instruction.Modulo,                   GenerateModulo);
             Add(Instruction.Multiply,                 GenerateMultiply);
             Add(Instruction.MultiplyHighS32,          GenerateMultiplyHighS32);
             Add(Instruction.MultiplyHighU32,          GenerateMultiplyHighU32);
@@ -744,8 +744,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
                 pCoords = Src(AggregateType.S32);
             }
 
-            pCoords = ScalingHelpers.ApplyScaling(context, texOp, pCoords, intCoords: true, isBindless, isIndexed, isArray, pCount);
-
             (var imageType, var imageVariable) = context.Images[new TextureMeta(texOp.CbufSlot, texOp.Handle, texOp.Format)];
 
             var image = context.Load(imageType, imageVariable);
@@ -1040,6 +1038,11 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
             return GenerateBinaryU32(context, operation, context.Delegates.GlslUMin);
         }
 
+        private static OperationResult GenerateModulo(CodeGenContext context, AstOperation operation)
+        {
+            return GenerateBinary(context, operation, context.Delegates.FMod, null);
+        }
+
         private static OperationResult GenerateMultiply(CodeGenContext context, AstOperation operation)
         {
             return GenerateBinary(context, operation, context.Delegates.FMul, context.Delegates.IMul);
@@ -1101,7 +1104,15 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
 
         private static OperationResult GenerateReturn(CodeGenContext context, AstOperation operation)
         {
-            context.Return();
+            if (operation.SourcesCount != 0)
+            {
+                context.ReturnValue(context.Get(context.CurrentFunction.ReturnType, operation.GetSource(0)));
+            }
+            else
+            {
+                context.Return();
+            }
+
             return OperationResult.Invalid;
         }
 
@@ -1439,35 +1450,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
                 }
             }
 
-            SpvInstruction ApplyBias(SpvInstruction vector, SpvInstruction image)
-            {
-                int gatherBiasPrecision = context.Config.GpuAccessor.QueryHostGatherBiasPrecision();
-                if (isGather && gatherBiasPrecision != 0)
-                {
-                    // GPU requires texture gather to be slightly offset to match NVIDIA behaviour when point is exactly between two texels.
-                    // Offset by the gather precision divided by 2 to correct for rounding.
-                    var sizeType = pCount == 1 ? context.TypeS32() : context.TypeVector(context.TypeS32(), pCount);
-                    var pVectorType = pCount == 1 ? context.TypeFP32() : context.TypeVector(context.TypeFP32(), pCount);
-
-                    var bias = context.Constant(context.TypeFP32(), (float)(1 << (gatherBiasPrecision + 1)));
-                    var biasVector = context.CompositeConstruct(pVectorType, Enumerable.Repeat(bias, pCount).ToArray());
-
-                    var one = context.Constant(context.TypeFP32(), 1f);
-                    var oneVector = context.CompositeConstruct(pVectorType, Enumerable.Repeat(one, pCount).ToArray());
-
-                    var divisor = context.FMul(
-                        pVectorType,
-                        context.ConvertSToF(pVectorType, context.ImageQuerySize(sizeType, image)),
-                        biasVector);
-
-                    vector = context.FAdd(pVectorType, vector, context.FDiv(pVectorType, oneVector, divisor));
-                }
-
-                return vector;
-            }
-
             SpvInstruction pCoords = AssemblePVector(pCount);
-            pCoords = ScalingHelpers.ApplyScaling(context, texOp, pCoords, intCoords, isBindless, isIndexed, isArray, pCount);
 
             SpvInstruction AssembleDerivativesVector(int count)
             {
@@ -1638,8 +1621,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
                 image = context.Image(imageType, image);
             }
 
-            pCoords = ApplyBias(pCoords, image);
-
             var operands = operandsList.ToArray();
 
             SpvInstruction result;
@@ -1755,11 +1736,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
                     result = context.CompositeExtract(context.TypeS32(), result, (SpvLiteralInteger)texOp.Index);
                 }
 
-                if (texOp.Index < 2 || (type & SamplerType.Mask) == SamplerType.Texture3D)
-                {
-                    result = ScalingHelpers.ApplyUnscaling(context, texOp.WithType(type), result, isBindless, isIndexed);
-                }
-
                 return new OperationResult(AggregateType.S32, result);
             }
         }
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/ScalingHelpers.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/ScalingHelpers.cs
deleted file mode 100644
index c8b21e8815..0000000000
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/ScalingHelpers.cs
+++ /dev/null
@@ -1,227 +0,0 @@
-using Ryujinx.Graphics.Shader.IntermediateRepresentation;
-using Ryujinx.Graphics.Shader.StructuredIr;
-using Ryujinx.Graphics.Shader.Translation;
-using static Spv.Specification;
-
-namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
-{
-    using SpvInstruction = Spv.Generator.Instruction;
-
-    static class ScalingHelpers
-    {
-        public static SpvInstruction ApplyScaling(
-            CodeGenContext context,
-            AstTextureOperation texOp,
-            SpvInstruction vector,
-            bool intCoords,
-            bool isBindless,
-            bool isIndexed,
-            bool isArray,
-            int pCount)
-        {
-            if (intCoords)
-            {
-                if (context.Config.Stage.SupportsRenderScale() &&
-                    !isBindless &&
-                    !isIndexed)
-                {
-                    int index = texOp.Inst == Instruction.ImageLoad
-                        ? context.Config.GetTextureDescriptors().Length + context.Config.FindImageDescriptorIndex(texOp)
-                        : context.Config.FindTextureDescriptorIndex(texOp);
-
-                    if (pCount == 3 && isArray)
-                    {
-                        return ApplyScaling2DArray(context, vector, index);
-                    }
-                    else if (pCount == 2 && !isArray)
-                    {
-                        return ApplyScaling2D(context, vector, index);
-                    }
-                }
-            }
-
-            return vector;
-        }
-
-        private static SpvInstruction ApplyScaling2DArray(CodeGenContext context, SpvInstruction vector, int index)
-        {
-            // The array index is not scaled, just x and y.
-            var vectorXY = context.VectorShuffle(context.TypeVector(context.TypeS32(), 2), vector, vector, 0, 1);
-            var vectorZ = context.CompositeExtract(context.TypeS32(), vector, 2);
-            var vectorXYScaled = ApplyScaling2D(context, vectorXY, index);
-            var vectorScaled = context.CompositeConstruct(context.TypeVector(context.TypeS32(), 3), vectorXYScaled, vectorZ);
-
-            return vectorScaled;
-        }
-
-        private static SpvInstruction ApplyScaling2D(CodeGenContext context, SpvInstruction vector, int index)
-        {
-            var pointerType = context.TypePointer(StorageClass.Uniform, context.TypeFP32());
-            var fieldIndex = context.Constant(context.TypeU32(), 4);
-            var scaleIndex = context.Constant(context.TypeU32(), index);
-
-            if (context.Config.Stage == ShaderStage.Vertex)
-            {
-                var scaleCountPointerType = context.TypePointer(StorageClass.Uniform, context.TypeS32());
-                var scaleCountElemPointer = context.AccessChain(scaleCountPointerType, context.ConstantBuffers[0], context.Constant(context.TypeU32(), 3));
-                var scaleCount = context.Load(context.TypeS32(), scaleCountElemPointer);
-
-                scaleIndex = context.IAdd(context.TypeU32(), scaleIndex, scaleCount);
-            }
-
-            scaleIndex = context.IAdd(context.TypeU32(), scaleIndex, context.Constant(context.TypeU32(), 1));
-
-            var scaleElemPointer = context.AccessChain(pointerType, context.ConstantBuffers[0], fieldIndex, scaleIndex);
-            var scale = context.Load(context.TypeFP32(), scaleElemPointer);
-
-            var ivector2Type = context.TypeVector(context.TypeS32(), 2);
-            var localVector = context.CoordTemp;
-
-            var passthrough = context.FOrdEqual(context.TypeBool(), scale, context.Constant(context.TypeFP32(), 1f));
-
-            var mergeLabel = context.Label();
-
-            if (context.Config.Stage == ShaderStage.Fragment)
-            {
-                var scaledInterpolatedLabel = context.Label();
-                var scaledNoInterpolationLabel = context.Label();
-
-                var needsInterpolation = context.FOrdLessThan(context.TypeBool(), scale, context.Constant(context.TypeFP32(), 0f));
-
-                context.SelectionMerge(mergeLabel, SelectionControlMask.MaskNone);
-                context.BranchConditional(needsInterpolation, scaledInterpolatedLabel, scaledNoInterpolationLabel);
-
-                // scale < 0.0
-                context.AddLabel(scaledInterpolatedLabel);
-
-                ApplyScalingInterpolated(context, localVector, vector, scale);
-                context.Branch(mergeLabel);
-
-                // scale >= 0.0
-                context.AddLabel(scaledNoInterpolationLabel);
-
-                ApplyScalingNoInterpolation(context, localVector, vector, scale);
-                context.Branch(mergeLabel);
-
-                context.AddLabel(mergeLabel);
-
-                var passthroughLabel = context.Label();
-                var finalMergeLabel = context.Label();
-
-                context.SelectionMerge(finalMergeLabel, SelectionControlMask.MaskNone);
-                context.BranchConditional(passthrough, passthroughLabel, finalMergeLabel);
-
-                context.AddLabel(passthroughLabel);
-
-                context.Store(localVector, vector);
-                context.Branch(finalMergeLabel);
-
-                context.AddLabel(finalMergeLabel);
-
-                return context.Load(ivector2Type, localVector);
-            }
-            else
-            {
-                var passthroughLabel = context.Label();
-                var scaledLabel = context.Label();
-
-                context.SelectionMerge(mergeLabel, SelectionControlMask.MaskNone);
-                context.BranchConditional(passthrough, passthroughLabel, scaledLabel);
-
-                // scale == 1.0
-                context.AddLabel(passthroughLabel);
-
-                context.Store(localVector, vector);
-                context.Branch(mergeLabel);
-
-                // scale != 1.0
-                context.AddLabel(scaledLabel);
-
-                ApplyScalingNoInterpolation(context, localVector, vector, scale);
-                context.Branch(mergeLabel);
-
-                context.AddLabel(mergeLabel);
-
-                return context.Load(ivector2Type, localVector);
-            }
-        }
-
-        private static void ApplyScalingInterpolated(CodeGenContext context, SpvInstruction output, SpvInstruction vector, SpvInstruction scale)
-        {
-            var vector2Type = context.TypeVector(context.TypeFP32(), 2);
-
-            var scaleNegated = context.FNegate(context.TypeFP32(), scale);
-            var scaleVector = context.CompositeConstruct(vector2Type, scaleNegated, scaleNegated);
-
-            var vectorFloat = context.ConvertSToF(vector2Type, vector);
-            var vectorScaled = context.VectorTimesScalar(vector2Type, vectorFloat, scaleNegated);
-
-            var fragCoordPointer = context.Inputs[new IoDefinition(StorageKind.Input, IoVariable.FragmentCoord)];
-            var fragCoord = context.Load(context.TypeVector(context.TypeFP32(), 4), fragCoordPointer);
-            var fragCoordXY = context.VectorShuffle(vector2Type, fragCoord, fragCoord, 0, 1);
-
-            var scaleMod = context.FMod(vector2Type, fragCoordXY, scaleVector);
-            var vectorInterpolated = context.FAdd(vector2Type, vectorScaled, scaleMod);
-
-            context.Store(output, context.ConvertFToS(context.TypeVector(context.TypeS32(), 2), vectorInterpolated));
-        }
-
-        private static void ApplyScalingNoInterpolation(CodeGenContext context, SpvInstruction output, SpvInstruction vector, SpvInstruction scale)
-        {
-            if (context.Config.Stage == ShaderStage.Vertex)
-            {
-                scale = context.GlslFAbs(context.TypeFP32(), scale);
-            }
-
-            var vector2Type = context.TypeVector(context.TypeFP32(), 2);
-
-            var vectorFloat = context.ConvertSToF(vector2Type, vector);
-            var vectorScaled = context.VectorTimesScalar(vector2Type, vectorFloat, scale);
-
-            context.Store(output, context.ConvertFToS(context.TypeVector(context.TypeS32(), 2), vectorScaled));
-        }
-
-        public static SpvInstruction ApplyUnscaling(
-            CodeGenContext context,
-            AstTextureOperation texOp,
-            SpvInstruction size,
-            bool isBindless,
-            bool isIndexed)
-        {
-            if (context.Config.Stage.SupportsRenderScale() &&
-                !isBindless &&
-                !isIndexed)
-            {
-                int index = context.Config.FindTextureDescriptorIndex(texOp);
-
-                var pointerType = context.TypePointer(StorageClass.Uniform, context.TypeFP32());
-                var fieldIndex = context.Constant(context.TypeU32(), 4);
-                var scaleIndex = context.Constant(context.TypeU32(), index);
-
-                if (context.Config.Stage == ShaderStage.Vertex)
-                {
-                    var scaleCountPointerType = context.TypePointer(StorageClass.Uniform, context.TypeS32());
-                    var scaleCountElemPointer = context.AccessChain(scaleCountPointerType, context.ConstantBuffers[0], context.Constant(context.TypeU32(), 3));
-                    var scaleCount = context.Load(context.TypeS32(), scaleCountElemPointer);
-
-                    scaleIndex = context.IAdd(context.TypeU32(), scaleIndex, scaleCount);
-                }
-
-                scaleIndex = context.IAdd(context.TypeU32(), scaleIndex, context.Constant(context.TypeU32(), 1));
-
-                var scaleElemPointer = context.AccessChain(pointerType, context.ConstantBuffers[0], fieldIndex, scaleIndex);
-                var scale = context.GlslFAbs(context.TypeFP32(), context.Load(context.TypeFP32(), scaleElemPointer));
-
-                var passthrough = context.FOrdEqual(context.TypeBool(), scale, context.Constant(context.TypeFP32(), 1f));
-
-                var sizeFloat = context.ConvertSToF(context.TypeFP32(), size);
-                var sizeUnscaled = context.FDiv(context.TypeFP32(), sizeFloat, scale);
-                var sizeUnscaledInt = context.ConvertFToS(context.TypeS32(), sizeUnscaled);
-
-                return context.Select(context.TypeS32(), passthrough, size, sizeUnscaledInt);
-            }
-
-            return size;
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/SpirvDelegates.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/SpirvDelegates.cs
index 3ccfd7f559..0fa954e154 100644
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/SpirvDelegates.cs
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/SpirvDelegates.cs
@@ -67,6 +67,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
         public readonly FuncBinaryInstruction GlslSMax;
         public readonly FuncBinaryInstruction GlslFMin;
         public readonly FuncBinaryInstruction GlslSMin;
+        public readonly FuncBinaryInstruction FMod;
         public readonly FuncBinaryInstruction FMul;
         public readonly FuncBinaryInstruction IMul;
         public readonly FuncBinaryInstruction FSub;
@@ -174,6 +175,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
             GlslSMax = context.GlslSMax;
             GlslFMin = context.GlslFMin;
             GlslSMin = context.GlslSMin;
+            FMod = context.FMod;
             FMul = context.FMul;
             IMul = context.IMul;
             FSub = context.FSub;
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/SpirvGenerator.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/SpirvGenerator.cs
index 3e11a97499..a55e09fd3f 100644
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/SpirvGenerator.cs
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/SpirvGenerator.cs
@@ -144,10 +144,9 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
 
         private static void Generate(CodeGenContext context, StructuredProgramInfo info, int funcIndex)
         {
-            var function = info.Functions[funcIndex];
-
-            (_, var spvFunc) = context.GetFunction(funcIndex);
+            (var function, var spvFunc) = context.GetFunction(funcIndex);
 
+            context.CurrentFunction = function;
             context.AddFunction(spvFunc);
             context.StartFunction();
 
diff --git a/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/Instruction.cs b/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/Instruction.cs
index 817755bbe0..f7afe50712 100644
--- a/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/Instruction.cs
+++ b/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/Instruction.cs
@@ -97,6 +97,7 @@ namespace Ryujinx.Graphics.Shader.IntermediateRepresentation
         MemoryBarrier,
         Minimum,
         MinimumU32,
+        Modulo,
         Multiply,
         MultiplyHighS32,
         MultiplyHighU32,
diff --git a/src/Ryujinx.Graphics.Shader/Ryujinx.Graphics.Shader.csproj b/src/Ryujinx.Graphics.Shader/Ryujinx.Graphics.Shader.csproj
index 3434e2a811..2efcbca4fb 100644
--- a/src/Ryujinx.Graphics.Shader/Ryujinx.Graphics.Shader.csproj
+++ b/src/Ryujinx.Graphics.Shader/Ryujinx.Graphics.Shader.csproj
@@ -4,10 +4,6 @@
     <TargetFramework>net7.0</TargetFramework>
   </PropertyGroup>
 
-  <ItemGroup>
-    <None Remove="CodeGen\Glsl\HelperFunctions\TexelFetchScale_vp.glsl" />
-  </ItemGroup>
-
   <ItemGroup>
     <ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" />
     <ProjectReference Include="..\Spv.Generator\Spv.Generator.csproj" />
@@ -25,9 +21,6 @@
     <EmbeddedResource Include="CodeGen\Glsl\HelperFunctions\StoreSharedSmallInt.glsl" />
     <EmbeddedResource Include="CodeGen\Glsl\HelperFunctions\StoreStorageSmallInt.glsl" />
     <EmbeddedResource Include="CodeGen\Glsl\HelperFunctions\SwizzleAdd.glsl" />
-    <EmbeddedResource Include="CodeGen\Glsl\HelperFunctions\TexelFetchScale_vp.glsl" />
-    <EmbeddedResource Include="CodeGen\Glsl\HelperFunctions\TexelFetchScale_fp.glsl" />
-    <EmbeddedResource Include="CodeGen\Glsl\HelperFunctions\TexelFetchScale_cp.glsl" />
   </ItemGroup>
 
 </Project>
diff --git a/src/Ryujinx.Graphics.Shader/StructuredIr/AstTextureOperation.cs b/src/Ryujinx.Graphics.Shader/StructuredIr/AstTextureOperation.cs
index a44f13cc09..6c27279c94 100644
--- a/src/Ryujinx.Graphics.Shader/StructuredIr/AstTextureOperation.cs
+++ b/src/Ryujinx.Graphics.Shader/StructuredIr/AstTextureOperation.cs
@@ -27,10 +27,5 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
             CbufSlot = cbufSlot;
             Handle = handle;
         }
-
-        public AstTextureOperation WithType(SamplerType type)
-        {
-            return new AstTextureOperation(Inst, type, Format, Flags, CbufSlot, Handle, Index);
-        }
     }
 }
\ No newline at end of file
diff --git a/src/Ryujinx.Graphics.Shader/StructuredIr/InstructionInfo.cs b/src/Ryujinx.Graphics.Shader/StructuredIr/InstructionInfo.cs
index ab81325409..44f0fad952 100644
--- a/src/Ryujinx.Graphics.Shader/StructuredIr/InstructionInfo.cs
+++ b/src/Ryujinx.Graphics.Shader/StructuredIr/InstructionInfo.cs
@@ -104,6 +104,7 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
             Add(Instruction.MaximumU32,               AggregateType.U32,    AggregateType.U32,     AggregateType.U32);
             Add(Instruction.Minimum,                  AggregateType.Scalar, AggregateType.Scalar,  AggregateType.Scalar);
             Add(Instruction.MinimumU32,               AggregateType.U32,    AggregateType.U32,     AggregateType.U32);
+            Add(Instruction.Modulo,                   AggregateType.Scalar, AggregateType.Scalar,  AggregateType.Scalar);
             Add(Instruction.Multiply,                 AggregateType.Scalar, AggregateType.Scalar,  AggregateType.Scalar);
             Add(Instruction.MultiplyHighS32,          AggregateType.S32,    AggregateType.S32,     AggregateType.S32);
             Add(Instruction.MultiplyHighU32,          AggregateType.U32,    AggregateType.U32,     AggregateType.U32);
diff --git a/src/Ryujinx.Graphics.Shader/StructuredIr/StructuredProgram.cs b/src/Ryujinx.Graphics.Shader/StructuredIr/StructuredProgram.cs
index 939a52f3e2..6846245e61 100644
--- a/src/Ryujinx.Graphics.Shader/StructuredIr/StructuredProgram.cs
+++ b/src/Ryujinx.Graphics.Shader/StructuredIr/StructuredProgram.cs
@@ -8,11 +8,11 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
 {
     static class StructuredProgram
     {
-        public static StructuredProgramInfo MakeStructuredProgram(Function[] functions, ShaderConfig config)
+        public static StructuredProgramInfo MakeStructuredProgram(IReadOnlyList<Function> functions, ShaderConfig config)
         {
             StructuredProgramContext context = new StructuredProgramContext(config);
 
-            for (int funcIndex = 0; funcIndex < functions.Length; funcIndex++)
+            for (int funcIndex = 0; funcIndex < functions.Count; funcIndex++)
             {
                 Function function = functions[funcIndex];
 
diff --git a/src/Ryujinx.Graphics.Shader/Translation/EmitterContext.cs b/src/Ryujinx.Graphics.Shader/Translation/EmitterContext.cs
index 0c51b16f4b..2786caaa0a 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/EmitterContext.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/EmitterContext.cs
@@ -49,13 +49,17 @@ namespace Ryujinx.Graphics.Shader.Translation
         private readonly List<Operation> _operations;
         private readonly Dictionary<ulong, BlockLabel> _labels;
 
-        public EmitterContext(DecodedProgram program, ShaderConfig config, bool isNonMain)
+        public EmitterContext()
+        {
+            _operations = new List<Operation>();
+            _labels = new Dictionary<ulong, BlockLabel>();
+        }
+
+        public EmitterContext(DecodedProgram program, ShaderConfig config, bool isNonMain) : this()
         {
             Program = program;
             Config = config;
             IsNonMain = isNonMain;
-            _operations = new List<Operation>();
-            _labels = new Dictionary<ulong, BlockLabel>();
 
             EmitStart();
         }
diff --git a/src/Ryujinx.Graphics.Shader/Translation/EmitterContextInsts.cs b/src/Ryujinx.Graphics.Shader/Translation/EmitterContextInsts.cs
index e41a28f121..6d4104ceeb 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/EmitterContextInsts.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/EmitterContextInsts.cs
@@ -307,6 +307,11 @@ namespace Ryujinx.Graphics.Shader.Translation
             return context.Add(fpType | Instruction.Minimum, Local(), a, b);
         }
 
+        public static Operand FPModulo(this EmitterContext context, Operand a, Operand b, Instruction fpType = Instruction.FP32)
+        {
+            return context.Add(fpType | Instruction.Modulo, Local(), a, b);
+        }
+
         public static Operand FPMultiply(this EmitterContext context, Operand a, Operand b, Instruction fpType = Instruction.FP32)
         {
             return context.Add(fpType | Instruction.Multiply, Local(), a, b);
@@ -656,7 +661,6 @@ namespace Ryujinx.Graphics.Shader.Translation
 
         public static void Return(this EmitterContext context, Operand returnValue)
         {
-            context.PrepareForReturn();
             context.Add(Instruction.Return, null, returnValue);
         }
 
diff --git a/src/Ryujinx.Graphics.Shader/Translation/HelperFunctionManager.cs b/src/Ryujinx.Graphics.Shader/Translation/HelperFunctionManager.cs
new file mode 100644
index 0000000000..206facd467
--- /dev/null
+++ b/src/Ryujinx.Graphics.Shader/Translation/HelperFunctionManager.cs
@@ -0,0 +1,134 @@
+using Ryujinx.Graphics.Shader.IntermediateRepresentation;
+using System;
+using System.Collections.Generic;
+
+using static Ryujinx.Graphics.Shader.IntermediateRepresentation.OperandHelper;
+
+namespace Ryujinx.Graphics.Shader.Translation
+{
+    class HelperFunctionManager
+    {
+        private readonly List<Function> _functionList;
+        private readonly Dictionary<HelperFunctionName, int> _functionIds;
+        private readonly ShaderStage _stage;
+
+        public HelperFunctionManager(List<Function> functionList, ShaderStage stage)
+        {
+            _functionList = functionList;
+            _functionIds = new Dictionary<HelperFunctionName, int>();
+            _stage = stage;
+        }
+
+        public int GetOrCreateFunctionId(HelperFunctionName functionName)
+        {
+            if (_functionIds.TryGetValue(functionName, out int functionId))
+            {
+                return functionId;
+            }
+
+            Function function = GenerateFunction(functionName);
+            functionId = _functionList.Count;
+            _functionList.Add(function);
+            _functionIds.Add(functionName, functionId);
+
+            return functionId;
+        }
+
+        private Function GenerateFunction(HelperFunctionName functionName)
+        {
+            return functionName switch
+            {
+                HelperFunctionName.TexelFetchScale => GenerateTexelFetchScaleFunction(),
+                HelperFunctionName.TextureSizeUnscale => GenerateTextureSizeUnscaleFunction(),
+                _ => throw new ArgumentException($"Invalid function name {functionName}")
+            };
+        }
+
+        private Function GenerateTexelFetchScaleFunction()
+        {
+            EmitterContext context = new EmitterContext();
+
+            Operand input = Argument(0);
+            Operand samplerIndex = Argument(1);
+            Operand index = GetScaleIndex(context, samplerIndex);
+
+            Operand scale = context.Load(StorageKind.ConstantBuffer, 0, Const((int)SupportBufferField.RenderScale), index);
+
+            Operand scaleIsOne = context.FPCompareEqual(scale, ConstF(1f));
+            Operand lblScaleNotOne = Label();
+
+            context.BranchIfFalse(lblScaleNotOne, scaleIsOne);
+            context.Return(input);
+            context.MarkLabel(lblScaleNotOne);
+
+            int inArgumentsCount;
+
+            if (_stage == ShaderStage.Fragment)
+            {
+                Operand scaleIsLessThanZero = context.FPCompareLess(scale, ConstF(0f));
+                Operand lblScaleGreaterOrEqualZero = Label();
+
+                context.BranchIfFalse(lblScaleGreaterOrEqualZero, scaleIsLessThanZero);
+
+                Operand negScale = context.FPNegate(scale);
+                Operand inputScaled = context.FPMultiply(context.IConvertS32ToFP32(input), negScale);
+                Operand fragCoordX = context.Load(StorageKind.Input, IoVariable.FragmentCoord, null, Const(0));
+                Operand fragCoordY = context.Load(StorageKind.Input, IoVariable.FragmentCoord, null, Const(1));
+                Operand fragCoord = context.ConditionalSelect(Argument(2), fragCoordY, fragCoordX);
+                Operand inputBias = context.FPModulo(fragCoord, negScale);
+                Operand inputWithBias = context.FPAdd(inputScaled, inputBias);
+
+                context.Return(context.FP32ConvertToS32(inputWithBias));
+                context.MarkLabel(lblScaleGreaterOrEqualZero);
+
+                inArgumentsCount = 3;
+            }
+            else
+            {
+                inArgumentsCount = 2;
+            }
+
+            Operand inputScaled2 = context.FPMultiply(context.IConvertS32ToFP32(input), scale);
+
+            context.Return(context.FP32ConvertToS32(inputScaled2));
+
+            return new Function(ControlFlowGraph.Create(context.GetOperations()).Blocks, "TexelFetchScale", true, inArgumentsCount, 0);
+        }
+
+        private Function GenerateTextureSizeUnscaleFunction()
+        {
+            EmitterContext context = new EmitterContext();
+
+            Operand input = Argument(0);
+            Operand samplerIndex = Argument(1);
+            Operand index = GetScaleIndex(context, samplerIndex);
+
+            Operand scale = context.FPAbsolute(context.Load(StorageKind.ConstantBuffer, 0, Const((int)SupportBufferField.RenderScale), index));
+
+            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, "TextureSizeUnscale", true, 2, 0);
+        }
+
+        private Operand GetScaleIndex(EmitterContext context, Operand index)
+        {
+            switch (_stage)
+            {
+                case ShaderStage.Vertex:
+                    Operand fragScaleCount = context.Load(StorageKind.ConstantBuffer, 0, Const((int)SupportBufferField.FragmentRenderScaleCount));
+                    return context.IAdd(Const(1), context.IAdd(index, fragScaleCount));
+                default:
+                    return context.IAdd(Const(1), index);
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Ryujinx.Graphics.Shader/Translation/HelperFunctionName.cs b/src/Ryujinx.Graphics.Shader/Translation/HelperFunctionName.cs
new file mode 100644
index 0000000000..5accdf65fb
--- /dev/null
+++ b/src/Ryujinx.Graphics.Shader/Translation/HelperFunctionName.cs
@@ -0,0 +1,11 @@
+using Ryujinx.Graphics.Shader.IntermediateRepresentation;
+using System.Collections.Generic;
+
+namespace Ryujinx.Graphics.Shader.Translation
+{
+    enum HelperFunctionName
+    {
+        TexelFetchScale,
+        TextureSizeUnscale
+    }
+}
\ No newline at end of file
diff --git a/src/Ryujinx.Graphics.Shader/Translation/Rewriter.cs b/src/Ryujinx.Graphics.Shader/Translation/Rewriter.cs
index 711661c9ba..6b55a484b9 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/Rewriter.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/Rewriter.cs
@@ -11,7 +11,7 @@ namespace Ryujinx.Graphics.Shader.Translation
 {
     static class Rewriter
     {
-        public static void RunPass(BasicBlock[] blocks, ShaderConfig config)
+        public static void RunPass(HelperFunctionManager hfm, BasicBlock[] blocks, ShaderConfig config)
         {
             bool isVertexShader = config.Stage == ShaderStage.Vertex;
             bool hasConstantBufferDrawParameters = config.GpuAccessor.QueryHasConstantBufferDrawParameters();
@@ -54,9 +54,14 @@ namespace Ryujinx.Graphics.Shader.Translation
 
                     if (operation is TextureOperation texOp)
                     {
+                        node = InsertTexelFetchScale(hfm, node, config);
+                        node = InsertTextureSizeUnscale(hfm, node, config);
+
                         if (texOp.Inst == Instruction.TextureSample)
                         {
-                            node = RewriteTextureSample(node, config);
+                            node = InsertCoordNormalization(node, config);
+                            node = InsertCoordGatherBias(node, config);
+                            node = InsertConstOffsets(node, config);
 
                             if (texOp.Type == SamplerType.TextureBuffer && !supportsSnormBufferTextureFormat)
                             {
@@ -344,10 +349,279 @@ namespace Ryujinx.Graphics.Shader.Translation
             return node;
         }
 
-        private static LinkedListNode<INode> RewriteTextureSample(LinkedListNode<INode> node, ShaderConfig config)
+        private static LinkedListNode<INode> InsertTexelFetchScale(HelperFunctionManager hfm, LinkedListNode<INode> node, ShaderConfig config)
         {
             TextureOperation texOp = (TextureOperation)node.Value;
 
+            bool isBindless = (texOp.Flags & TextureFlags.Bindless)  != 0;
+            bool intCoords  = (texOp.Flags & TextureFlags.IntCoords) != 0;
+
+            bool isArray   = (texOp.Type & SamplerType.Array)   != 0;
+            bool isIndexed = (texOp.Type & SamplerType.Indexed) != 0;
+
+            int coordsCount = texOp.Type.GetDimensions();
+
+            int coordsIndex = isBindless || isIndexed ? 1 : 0;
+
+            bool isImage = IsImageInstructionWithScale(texOp.Inst);
+
+            if ((texOp.Inst == Instruction.TextureSample || isImage) &&
+                intCoords &&
+                !isBindless &&
+                !isIndexed &&
+                config.Stage.SupportsRenderScale() &&
+                TypeSupportsScale(texOp.Type))
+            {
+                int functionId = hfm.GetOrCreateFunctionId(HelperFunctionName.TexelFetchScale);
+                int samplerIndex = isImage
+                    ? config.GetTextureDescriptors().Length + config.FindImageDescriptorIndex(texOp)
+                    : config.FindTextureDescriptorIndex(texOp);
+
+                for (int index = 0; index < coordsCount; index++)
+                {
+                    Operand scaledCoord = Local();
+                    Operand[] callArgs;
+
+                    if (config.Stage == ShaderStage.Fragment)
+                    {
+                        callArgs = new Operand[] { Const(functionId), texOp.GetSource(coordsIndex + index), Const(samplerIndex), Const(index) };
+                    }
+                    else
+                    {
+                        callArgs = new Operand[] { Const(functionId), texOp.GetSource(coordsIndex + index), Const(samplerIndex) };
+                    }
+
+                    node.List.AddBefore(node, new Operation(Instruction.Call, 0, scaledCoord, callArgs));
+
+                    texOp.SetSource(coordsIndex + index, scaledCoord);
+                }
+            }
+
+            return node;
+        }
+
+        private static LinkedListNode<INode> InsertTextureSizeUnscale(HelperFunctionManager hfm, LinkedListNode<INode> node, ShaderConfig config)
+        {
+            TextureOperation texOp = (TextureOperation)node.Value;
+
+            bool isBindless = (texOp.Flags & TextureFlags.Bindless)  != 0;
+            bool intCoords  = (texOp.Flags & TextureFlags.IntCoords) != 0;
+
+            bool isArray   = (texOp.Type & SamplerType.Array)   != 0;
+            bool isIndexed = (texOp.Type & SamplerType.Indexed) != 0;
+
+            if (texOp.Inst == Instruction.TextureSize &&
+                texOp.Index < 2 &&
+                !isBindless &&
+                !isIndexed &&
+                config.Stage.SupportsRenderScale() &&
+                TypeSupportsScale(texOp.Type))
+            {
+                int functionId = hfm.GetOrCreateFunctionId(HelperFunctionName.TextureSizeUnscale);
+                int samplerIndex = config.FindTextureDescriptorIndex(texOp, ignoreType: true);
+
+                for (int index = texOp.DestsCount - 1; index >= 0; index--)
+                {
+                    Operand dest = texOp.GetDest(index);
+
+                    Operand unscaledSize = Local();
+
+                    // Replace all uses with the unscaled size value.
+                    // This must be done before the call is added, since it also is a use of the original size.
+                    foreach (INode useOp in dest.UseOps)
+                    {
+                        for (int srcIndex = 0; srcIndex < useOp.SourcesCount; srcIndex++)
+                        {
+                            if (useOp.GetSource(srcIndex) == dest)
+                            {
+                                useOp.SetSource(srcIndex, unscaledSize);
+                            }
+                        }
+                    }
+
+                    Operand[] callArgs = new Operand[] { Const(functionId), dest, Const(samplerIndex) };
+
+                    node.List.AddAfter(node, new Operation(Instruction.Call, 0, unscaledSize, callArgs));
+                }
+            }
+
+            return node;
+        }
+
+        private static bool IsImageInstructionWithScale(Instruction inst)
+        {
+            // Currently, we don't support scaling images that are modified,
+            // so we only need to care about the load instruction.
+            return inst == Instruction.ImageLoad;
+        }
+
+        private static bool TypeSupportsScale(SamplerType type)
+        {
+            return (type & SamplerType.Mask) == SamplerType.Texture2D;
+        }
+
+        private static LinkedListNode<INode> InsertCoordNormalization(LinkedListNode<INode> node, ShaderConfig config)
+        {
+            // Emulate non-normalized coordinates by normalizing the coordinates on the shader.
+            // Without normalization, the coordinates are expected to the in the [0, W or H] range,
+            // and otherwise, it is expected to be in the [0, 1] range.
+            // We normalize by dividing the coords by the texture size.
+
+            TextureOperation texOp = (TextureOperation)node.Value;
+
+            bool isBindless = (texOp.Flags & TextureFlags.Bindless)  != 0;
+            bool intCoords  = (texOp.Flags & TextureFlags.IntCoords) != 0;
+
+            bool isCoordNormalized = isBindless || config.GpuAccessor.QueryTextureCoordNormalized(texOp.Handle, texOp.CbufSlot);
+
+            if (isCoordNormalized || intCoords)
+            {
+                return node;
+            }
+
+            bool isArray   = (texOp.Type & SamplerType.Array)   != 0;
+            bool isIndexed = (texOp.Type & SamplerType.Indexed) != 0;
+
+            int coordsCount = texOp.Type.GetDimensions();
+            int coordsIndex = isBindless || isIndexed ? 1 : 0;
+
+            config.SetUsedFeature(FeatureFlags.IntegerSampling);
+
+            int normCoordsCount = (texOp.Type & SamplerType.Mask) == SamplerType.TextureCube ? 2 : coordsCount;
+
+            for (int index = 0; index < normCoordsCount; index++)
+            {
+                Operand coordSize = Local();
+
+                Operand[] texSizeSources;
+
+                if (isBindless || isIndexed)
+                {
+                    texSizeSources = new Operand[] { texOp.GetSource(0), Const(0) };
+                }
+                else
+                {
+                    texSizeSources = new Operand[] { Const(0) };
+                }
+
+                node.List.AddBefore(node, new TextureOperation(
+                    Instruction.TextureSize,
+                    texOp.Type,
+                    texOp.Format,
+                    texOp.Flags,
+                    texOp.CbufSlot,
+                    texOp.Handle,
+                    index,
+                    new[] { coordSize },
+                    texSizeSources));
+
+                config.SetUsedTexture(Instruction.TextureSize, texOp.Type, texOp.Format, texOp.Flags, texOp.CbufSlot, texOp.Handle);
+
+                Operand source = texOp.GetSource(coordsIndex + index);
+
+                Operand coordNormalized = Local();
+
+                node.List.AddBefore(node, new Operation(Instruction.FP32 | Instruction.Divide, coordNormalized, source, GenerateI2f(node, coordSize)));
+
+                texOp.SetSource(coordsIndex + index, coordNormalized);
+            }
+
+            return node;
+        }
+
+        private static LinkedListNode<INode> InsertCoordGatherBias(LinkedListNode<INode> node, ShaderConfig config)
+        {
+            // The gather behavior when the coordinate sits right in the middle of two texels is not well defined.
+            // To ensure the correct texel is sampled, we add a small bias value to the coordinate.
+            // This value is calculated as the minimum value required to change the texel it will sample from,
+            // and is 0 if the host does not require the bias.
+
+            TextureOperation texOp = (TextureOperation)node.Value;
+
+            bool isBindless = (texOp.Flags & TextureFlags.Bindless) != 0;
+            bool isGather   = (texOp.Flags & TextureFlags.Gather)   != 0;
+
+            int gatherBiasPrecision = config.GpuAccessor.QueryHostGatherBiasPrecision();
+
+            if (!isGather || gatherBiasPrecision == 0)
+            {
+                return node;
+            }
+
+            bool intCoords = (texOp.Flags & TextureFlags.IntCoords) != 0;
+
+            bool isArray   = (texOp.Type & SamplerType.Array)   != 0;
+            bool isIndexed = (texOp.Type & SamplerType.Indexed) != 0;
+
+            int coordsCount = texOp.Type.GetDimensions();
+            int coordsIndex = isBindless || isIndexed ? 1 : 0;
+
+            config.SetUsedFeature(FeatureFlags.IntegerSampling);
+
+            int normCoordsCount = (texOp.Type & SamplerType.Mask) == SamplerType.TextureCube ? 2 : coordsCount;
+
+            for (int index = 0; index < normCoordsCount; index++)
+            {
+                Operand coordSize = Local();
+                Operand scaledSize = Local();
+                Operand bias = Local();
+
+                Operand[] texSizeSources;
+
+                if (isBindless || isIndexed)
+                {
+                    texSizeSources = new Operand[] { texOp.GetSource(0), Const(0) };
+                }
+                else
+                {
+                    texSizeSources = new Operand[] { Const(0) };
+                }
+
+                node.List.AddBefore(node, new TextureOperation(
+                    Instruction.TextureSize,
+                    texOp.Type,
+                    texOp.Format,
+                    texOp.Flags,
+                    texOp.CbufSlot,
+                    texOp.Handle,
+                    index,
+                    new[] { coordSize },
+                    texSizeSources));
+
+                config.SetUsedTexture(Instruction.TextureSize, texOp.Type, texOp.Format, texOp.Flags, texOp.CbufSlot, texOp.Handle);
+
+                node.List.AddBefore(node, new Operation(
+                    Instruction.FP32 | Instruction.Multiply,
+                    scaledSize,
+                    GenerateI2f(node, coordSize),
+                    ConstF((float)(1 << (gatherBiasPrecision + 1)))));
+                node.List.AddBefore(node, new Operation(Instruction.FP32 | Instruction.Divide, bias, ConstF(1f), scaledSize));
+
+                Operand source = texOp.GetSource(coordsIndex + index);
+
+                Operand coordBiased = Local();
+
+                node.List.AddBefore(node, new Operation(Instruction.FP32 | Instruction.Add, coordBiased, source, bias));
+
+                texOp.SetSource(coordsIndex + index, coordBiased);
+            }
+
+            return node;
+        }
+
+        private static LinkedListNode<INode> InsertConstOffsets(LinkedListNode<INode> node, ShaderConfig config)
+        {
+            // Non-constant texture offsets are not allowed (according to the spec),
+            // however some GPUs does support that.
+            // For GPUs where it is not supported, we can replace the instruction with the following:
+            // For texture*Offset, we replace it by texture*, and add the offset to the P coords.
+            // The offset can be calculated as offset / textureSize(lod), where lod = textureQueryLod(coords).
+            // For texelFetchOffset, we replace it by texelFetch and add the offset to the P coords directly.
+            // For textureGatherOffset, we split the operation into up to 4 operations, one for each component
+            // that is accessed, where each textureGather operation has a different offset for each pixel.
+
+            TextureOperation texOp = (TextureOperation)node.Value;
+
             bool hasOffset  = (texOp.Flags & TextureFlags.Offset)  != 0;
             bool hasOffsets = (texOp.Flags & TextureFlags.Offsets) != 0;
 
@@ -355,9 +629,7 @@ namespace Ryujinx.Graphics.Shader.Translation
 
             bool isBindless = (texOp.Flags & TextureFlags.Bindless) != 0;
 
-            bool isCoordNormalized = isBindless || config.GpuAccessor.QueryTextureCoordNormalized(texOp.Handle, texOp.CbufSlot);
-
-            if (!hasInvalidOffset && isCoordNormalized)
+            if (!hasInvalidOffset)
             {
                 return node;
             }
@@ -454,7 +726,7 @@ namespace Ryujinx.Graphics.Shader.Translation
 
             hasInvalidOffset &= !areAllOffsetsConstant;
 
-            if (!hasInvalidOffset && isCoordNormalized)
+            if (!hasInvalidOffset)
             {
                 return node;
             }
@@ -473,63 +745,6 @@ namespace Ryujinx.Graphics.Shader.Translation
 
             int componentIndex = texOp.Index;
 
-            Operand Float(Operand value)
-            {
-                Operand res = Local();
-
-                node.List.AddBefore(node, new Operation(Instruction.ConvertS32ToFP32, res, value));
-
-                return res;
-            }
-
-            // 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,
-            // and otherwise, it is expected to be in the [0, 1] range.
-            // We normalize by dividing the coords by the texture size.
-            if (!isCoordNormalized && !intCoords)
-            {
-                config.SetUsedFeature(FeatureFlags.IntegerSampling);
-
-                int normCoordsCount = (texOp.Type & SamplerType.Mask) == SamplerType.TextureCube ? 2 : coordsCount;
-
-                for (int index = 0; index < normCoordsCount; index++)
-                {
-                    Operand coordSize = Local();
-
-                    Operand[] texSizeSources;
-
-                    if (isBindless || isIndexed)
-                    {
-                        texSizeSources = new Operand[] { sources[0], Const(0) };
-                    }
-                    else
-                    {
-                        texSizeSources = new Operand[] { Const(0) };
-                    }
-
-                    node.List.AddBefore(node, new TextureOperation(
-                        Instruction.TextureSize,
-                        texOp.Type,
-                        texOp.Format,
-                        texOp.Flags,
-                        texOp.CbufSlot,
-                        texOp.Handle,
-                        index,
-                        new[] { coordSize },
-                        texSizeSources));
-
-                    config.SetUsedTexture(Instruction.TextureSize, texOp.Type, texOp.Format, texOp.Flags, texOp.CbufSlot, texOp.Handle);
-
-                    Operand source = sources[coordsIndex + index];
-
-                    Operand coordNormalized = Local();
-
-                    node.List.AddBefore(node, new Operation(Instruction.FP32 | Instruction.Divide, coordNormalized, source, Float(coordSize)));
-
-                    sources[coordsIndex + index] = coordNormalized;
-                }
-            }
-
             Operand[] dests = new Operand[texOp.DestsCount];
 
             for (int i = 0; i < texOp.DestsCount; i++)
@@ -541,15 +756,7 @@ namespace Ryujinx.Graphics.Shader.Translation
 
             LinkedListNode<INode> oldNode = node;
 
-            // Technically, non-constant texture offsets are not allowed (according to the spec),
-            // however some GPUs does support that.
-            // For GPUs where it is not supported, we can replace the instruction with the following:
-            // For texture*Offset, we replace it by texture*, and add the offset to the P coords.
-            // The offset can be calculated as offset / textureSize(lod), where lod = textureQueryLod(coords).
-            // For texelFetchOffset, we replace it by texelFetch and add the offset to the P coords directly.
-            // For textureGatherOffset, we split the operation into up to 4 operations, one for each component
-            // that is accessed, where each textureGather operation has a different offset for each pixel.
-            if (hasInvalidOffset && isGather && !isShadow)
+            if (isGather && !isShadow)
             {
                 config.SetUsedFeature(FeatureFlags.IntegerSampling);
 
@@ -557,7 +764,7 @@ namespace Ryujinx.Graphics.Shader.Translation
 
                 sources.CopyTo(newSources, 0);
 
-                Operand[] texSizes = InsertTextureSize(node, texOp, lodSources, bindlessHandle, coordsCount);
+                Operand[] texSizes = InsertTextureLod(node, texOp, lodSources, bindlessHandle, coordsCount);
 
                 int destIndex = 0;
 
@@ -576,7 +783,11 @@ namespace Ryujinx.Graphics.Shader.Translation
 
                         Operand intOffset = offsets[index + (hasOffsets ? compIndex * coordsCount : 0)];
 
-                        node.List.AddBefore(node, new Operation(Instruction.FP32 | Instruction.Divide, offset, Float(intOffset), Float(texSizes[index])));
+                        node.List.AddBefore(node, new Operation(
+                            Instruction.FP32 | Instruction.Divide,
+                            offset,
+                            GenerateI2f(node, intOffset),
+                            GenerateI2f(node, texSizes[index])));
 
                         Operand source = sources[coordsIndex + index];
 
@@ -603,45 +814,46 @@ namespace Ryujinx.Graphics.Shader.Translation
             }
             else
             {
-                if (hasInvalidOffset)
+                if (intCoords)
                 {
-                    if (intCoords)
+                    for (int index = 0; index < coordsCount; index++)
                     {
-                        for (int index = 0; index < coordsCount; index++)
-                        {
-                            Operand source = sources[coordsIndex + index];
+                        Operand source = sources[coordsIndex + index];
 
-                            Operand coordPlusOffset = Local();
+                        Operand coordPlusOffset = Local();
 
-                            node.List.AddBefore(node, new Operation(Instruction.Add, coordPlusOffset, source, offsets[index]));
+                        node.List.AddBefore(node, new Operation(Instruction.Add, coordPlusOffset, source, offsets[index]));
 
-                            sources[coordsIndex + index] = coordPlusOffset;
-                        }
+                        sources[coordsIndex + index] = coordPlusOffset;
                     }
-                    else
+                }
+                else
+                {
+                    config.SetUsedFeature(FeatureFlags.IntegerSampling);
+
+                    Operand[] texSizes = InsertTextureLod(node, texOp, lodSources, bindlessHandle, coordsCount);
+
+                    for (int index = 0; index < coordsCount; index++)
                     {
-                        config.SetUsedFeature(FeatureFlags.IntegerSampling);
+                        config.SetUsedTexture(Instruction.TextureSize, texOp.Type, texOp.Format, texOp.Flags, texOp.CbufSlot, texOp.Handle);
 
-                        Operand[] texSizes = InsertTextureSize(node, texOp, lodSources, bindlessHandle, coordsCount);
+                        Operand offset = Local();
 
-                        for (int index = 0; index < coordsCount; index++)
-                        {
-                            config.SetUsedTexture(Instruction.TextureSize, texOp.Type, texOp.Format, texOp.Flags, texOp.CbufSlot, texOp.Handle);
+                        Operand intOffset = offsets[index];
 
-                            Operand offset = Local();
+                        node.List.AddBefore(node, new Operation(
+                            Instruction.FP32 | Instruction.Divide,
+                            offset,
+                            GenerateI2f(node, intOffset),
+                            GenerateI2f(node, texSizes[index])));
 
-                            Operand intOffset = offsets[index];
+                        Operand source = sources[coordsIndex + index];
 
-                            node.List.AddBefore(node, new Operation(Instruction.FP32 | Instruction.Divide, offset, Float(intOffset), Float(texSizes[index])));
+                        Operand coordPlusOffset = Local();
 
-                            Operand source = sources[coordsIndex + index];
+                        node.List.AddBefore(node, new Operation(Instruction.FP32 | Instruction.Add, coordPlusOffset, source, offset));
 
-                            Operand coordPlusOffset = Local();
-
-                            node.List.AddBefore(node, new Operation(Instruction.FP32 | Instruction.Add, coordPlusOffset, source, offset));
-
-                            sources[coordsIndex + index] = coordPlusOffset;
-                        }
+                        sources[coordsIndex + index] = coordPlusOffset;
                     }
                 }
 
@@ -669,22 +881,13 @@ namespace Ryujinx.Graphics.Shader.Translation
             return node;
         }
 
-        private static Operand[] InsertTextureSize(
+        private static Operand[] InsertTextureLod(
             LinkedListNode<INode> node,
             TextureOperation texOp,
             Operand[] lodSources,
             Operand bindlessHandle,
             int coordsCount)
         {
-            Operand Int(Operand value)
-            {
-                Operand res = Local();
-
-                node.List.AddBefore(node, new Operation(Instruction.ConvertFP32ToS32, res, value));
-
-                return res;
-            }
-
             Operand[] texSizes = new Operand[coordsCount];
 
             Operand lod = Local();
@@ -708,11 +911,11 @@ namespace Ryujinx.Graphics.Shader.Translation
 
                 if (bindlessHandle != null)
                 {
-                    texSizeSources = new Operand[] { bindlessHandle, Int(lod) };
+                    texSizeSources = new Operand[] { bindlessHandle, GenerateF2i(node, lod) };
                 }
                 else
                 {
-                    texSizeSources = new Operand[] { Int(lod) };
+                    texSizeSources = new Operand[] { GenerateF2i(node, lod) };
                 }
 
                 node.List.AddBefore(node, new TextureOperation(
@@ -796,6 +999,24 @@ namespace Ryujinx.Graphics.Shader.Translation
             return node;
         }
 
+        private static Operand GenerateI2f(LinkedListNode<INode> node, Operand value)
+        {
+            Operand res = Local();
+
+            node.List.AddBefore(node, new Operation(Instruction.ConvertS32ToFP32, res, value));
+
+            return res;
+        }
+
+        private static Operand GenerateF2i(LinkedListNode<INode> node, Operand value)
+        {
+            Operand res = Local();
+
+            node.List.AddBefore(node, new Operation(Instruction.ConvertFP32ToS32, res, value));
+
+            return res;
+        }
+
         private static bool ReplaceConstantBufferWithDrawParameters(LinkedListNode<INode> node, Operation operation)
         {
             Operand GenerateLoad(IoVariable ioVariable)
diff --git a/src/Ryujinx.Graphics.Shader/Translation/ShaderConfig.cs b/src/Ryujinx.Graphics.Shader/Translation/ShaderConfig.cs
index 775607972e..73525cb27c 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/ShaderConfig.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/ShaderConfig.cs
@@ -860,7 +860,7 @@ namespace Ryujinx.Graphics.Shader.Translation
             return descriptors;
         }
 
-        public (TextureDescriptor, int) FindTextureDescriptor(AstTextureOperation texOp)
+        public TextureDescriptor FindTextureDescriptor(AstTextureOperation texOp)
         {
             TextureDescriptor[] descriptors = GetTextureDescriptors();
 
@@ -872,11 +872,11 @@ namespace Ryujinx.Graphics.Shader.Translation
                     descriptor.HandleIndex == texOp.Handle &&
                     descriptor.Format == texOp.Format)
                 {
-                    return (descriptor, i);
+                    return descriptor;
                 }
             }
 
-            return (default, -1);
+            return default;
         }
 
         private static int FindDescriptorIndex(TextureDescriptor[] array, AstTextureOperation texOp)
@@ -897,12 +897,30 @@ namespace Ryujinx.Graphics.Shader.Translation
             return -1;
         }
 
-        public int FindTextureDescriptorIndex(AstTextureOperation texOp)
+        private static int FindDescriptorIndex(TextureDescriptor[] array, TextureOperation texOp, bool ignoreType = false)
         {
-            return FindDescriptorIndex(GetTextureDescriptors(), texOp);
+            for (int i = 0; i < array.Length; i++)
+            {
+                var descriptor = array[i];
+
+                if ((descriptor.Type == texOp.Type || ignoreType) &&
+                    descriptor.CbufSlot == texOp.CbufSlot &&
+                    descriptor.HandleIndex == texOp.Handle &&
+                    descriptor.Format == texOp.Format)
+                {
+                    return i;
+                }
+            }
+
+            return -1;
         }
 
-        public int FindImageDescriptorIndex(AstTextureOperation texOp)
+        public int FindTextureDescriptorIndex(TextureOperation texOp, bool ignoreType = false)
+        {
+            return FindDescriptorIndex(GetTextureDescriptors(), texOp, ignoreType);
+        }
+
+        public int FindImageDescriptorIndex(TextureOperation texOp)
         {
             return FindDescriptorIndex(GetImageDescriptors(), texOp);
         }
diff --git a/src/Ryujinx.Graphics.Shader/Translation/ShaderIdentifier.cs b/src/Ryujinx.Graphics.Shader/Translation/ShaderIdentifier.cs
index 53f1e8475b..867e243795 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/ShaderIdentifier.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/ShaderIdentifier.cs
@@ -1,11 +1,11 @@
 using Ryujinx.Graphics.Shader.IntermediateRepresentation;
-using static Ryujinx.Graphics.Shader.IntermediateRepresentation.OperandHelper;
+using System.Collections.Generic;
 
 namespace Ryujinx.Graphics.Shader.Translation
 {
     static class ShaderIdentifier
     {
-        public static ShaderIdentification Identify(Function[] functions, ShaderConfig config)
+        public static ShaderIdentification Identify(IReadOnlyList<Function> functions, ShaderConfig config)
         {
             if (config.Stage == ShaderStage.Geometry &&
                 config.GpuAccessor.QueryPrimitiveTopology() == InputTopology.Triangles &&
@@ -20,12 +20,12 @@ namespace Ryujinx.Graphics.Shader.Translation
             return ShaderIdentification.None;
         }
 
-        private static bool IsLayerPassthroughGeometryShader(Function[] functions, out int layerInputAttr)
+        private static bool IsLayerPassthroughGeometryShader(IReadOnlyList<Function> functions, out int layerInputAttr)
         {
             bool writesLayer = false;
             layerInputAttr = 0;
 
-            if (functions.Length != 1)
+            if (functions.Count != 1)
             {
                 return false;
             }
diff --git a/src/Ryujinx.Graphics.Shader/Translation/Translator.cs b/src/Ryujinx.Graphics.Shader/Translation/Translator.cs
index 87d97e52ea..5bbc00097d 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/Translator.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/Translator.cs
@@ -5,6 +5,7 @@ using Ryujinx.Graphics.Shader.IntermediateRepresentation;
 using Ryujinx.Graphics.Shader.StructuredIr;
 using Ryujinx.Graphics.Shader.Translation.Optimizations;
 using System;
+using System.Collections.Generic;
 using System.Linq;
 using static Ryujinx.Graphics.Shader.IntermediateRepresentation.OperandHelper;
 
@@ -44,7 +45,14 @@ namespace Ryujinx.Graphics.Shader.Translation
                 }
             }
 
-            Function[] funcs = new Function[functions.Length];
+            List<Function> funcs = new List<Function>(functions.Length);
+
+            for (int i = 0; i < functions.Length; i++)
+            {
+                funcs.Add(null);
+            }
+
+            HelperFunctionManager hfm = new HelperFunctionManager(funcs, config.Stage);
 
             for (int i = 0; i < functions.Length; i++)
             {
@@ -71,7 +79,7 @@ namespace Ryujinx.Graphics.Shader.Translation
                     Ssa.Rename(cfg.Blocks);
 
                     Optimizer.RunPass(cfg.Blocks, config);
-                    Rewriter.RunPass(cfg.Blocks, config);
+                    Rewriter.RunPass(hfm, cfg.Blocks, config);
                 }
 
                 funcs[i] = new Function(cfg.Blocks, $"fun{i}", false, inArgumentsCount, outArgumentsCount);