From 9f12e50a546b15533778ed0d8290202af91c10a2 Mon Sep 17 00:00:00 2001
From: gdkchan <gab.dark.100@gmail.com>
Date: Tue, 25 Apr 2023 19:51:07 -0300
Subject: Refactor attribute handling on the shader generator (#4565)

* Refactor attribute handling on the shader generator

* Implement gl_ViewportMask[]

* Add back the Intel FrontFacing bug workaround

* Fix GLSL transform feedback outputs mistmatch with fragment stage

* Shader cache version bump

* Fix geometry shader recognition

* PR feedback

* Delete GetOperandDef and GetOperandUse

* Remove replacements that are no longer needed on GLSL compilation on Vulkan

* Fix incorrect load for per-patch outputs

* Fix build
---
 Ryujinx.Graphics.GAL/Capabilities.cs               |   9 +-
 .../Shader/DiskCache/DiskCacheHostStorage.cs       |   2 +-
 Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs     |   4 +-
 Ryujinx.Graphics.OpenGL/HwCapabilities.cs          |   2 +
 Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs          |   3 +-
 .../CodeGen/Glsl/Declarations.cs                   |  35 +-
 .../CodeGen/Glsl/GlslGenerator.cs                  |  16 +-
 .../CodeGen/Glsl/Instructions/InstGen.cs           |  20 +-
 .../CodeGen/Glsl/Instructions/InstGenHelper.cs     |   4 +-
 .../CodeGen/Glsl/Instructions/InstGenMemory.cs     | 158 ++++++---
 .../CodeGen/Glsl/Instructions/IoMap.cs             | 145 ++++++++
 .../CodeGen/Glsl/OperandManager.cs                 | 345 +++----------------
 .../CodeGen/Spirv/CodeGenContext.cs                | 255 +-------------
 .../CodeGen/Spirv/Declarations.cs                  | 380 ++++++---------------
 .../CodeGen/Spirv/EnumConversion.cs                |   3 +-
 .../CodeGen/Spirv/Instructions.cs                  | 241 +++++++++----
 Ryujinx.Graphics.Shader/CodeGen/Spirv/IoMap.cs     |  86 +++++
 .../CodeGen/Spirv/ScalingHelpers.cs                |   2 +-
 .../CodeGen/Spirv/SpirvGenerator.cs                |  31 +-
 Ryujinx.Graphics.Shader/Decoders/Decoder.cs        |   5 +-
 Ryujinx.Graphics.Shader/IGpuAccessor.cs            |  19 +-
 .../Instructions/AttributeMap.cs                   | 351 +++++++++++++++++++
 .../Instructions/InstEmitAttribute.cs              | 150 +++++---
 .../Instructions/InstEmitMemory.cs                 |  24 +-
 .../Instructions/InstEmitMove.cs                   |  36 +-
 .../IntermediateRepresentation/Instruction.cs      |  11 +-
 .../IntermediateRepresentation/IoVariable.cs       |  51 +++
 .../IntermediateRepresentation/OperandHelper.cs    |  10 -
 .../IntermediateRepresentation/OperandType.cs      |  10 -
 .../IntermediateRepresentation/Operation.cs        |  18 +
 .../IntermediateRepresentation/StorageKind.cs      |  39 +++
 .../StructuredIr/AstOperation.cs                   |  10 +-
 .../StructuredIr/AstTextureOperation.cs            |   2 +-
 .../StructuredIr/InstructionInfo.cs                |   6 +-
 .../StructuredIr/IoDefinition.cs                   |  44 +++
 .../StructuredIr/OperandInfo.cs                    |   2 -
 .../StructuredIr/StructuredProgram.cs              |  77 ++---
 .../StructuredIr/StructuredProgramContext.cs       |  67 +---
 .../StructuredIr/StructuredProgramInfo.cs          |  49 +--
 .../Translation/AttributeConsts.cs                 | 121 ++-----
 .../Translation/AttributeInfo.cs                   | 210 ------------
 .../Translation/EmitterContext.cs                  | 100 ++++--
 .../Translation/EmitterContextInsts.cs             | 109 ++++--
 .../Translation/GlobalMemory.cs                    |   9 +-
 .../Translation/Optimizations/GlobalToStorage.cs   |   6 +-
 .../Translation/Optimizations/Optimizer.cs         |   6 +-
 Ryujinx.Graphics.Shader/Translation/Rewriter.cs    |  23 +-
 .../Translation/ShaderConfig.cs                    | 211 +++++++++++-
 .../Translation/ShaderIdentifier.cs                |  72 +++-
 Ryujinx.Graphics.Shader/Translation/Translator.cs  |  57 +++-
 .../Translation/TranslatorContext.cs               |  47 ++-
 Ryujinx.Graphics.Vulkan/HardwareCapabilities.cs    |   3 +
 Ryujinx.Graphics.Vulkan/Shader.cs                  |   4 -
 Ryujinx.Graphics.Vulkan/VulkanInitialization.cs    |   3 +-
 Ryujinx.Graphics.Vulkan/VulkanRenderer.cs          |   4 +-
 Ryujinx.ShaderTools/Program.cs                     |   4 +-
 56 files changed, 1966 insertions(+), 1745 deletions(-)
 create mode 100644 Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/IoMap.cs
 create mode 100644 Ryujinx.Graphics.Shader/CodeGen/Spirv/IoMap.cs
 create mode 100644 Ryujinx.Graphics.Shader/Instructions/AttributeMap.cs
 create mode 100644 Ryujinx.Graphics.Shader/IntermediateRepresentation/IoVariable.cs
 create mode 100644 Ryujinx.Graphics.Shader/IntermediateRepresentation/StorageKind.cs
 create mode 100644 Ryujinx.Graphics.Shader/StructuredIr/IoDefinition.cs
 delete mode 100644 Ryujinx.Graphics.Shader/Translation/AttributeInfo.cs

diff --git a/Ryujinx.Graphics.GAL/Capabilities.cs b/Ryujinx.Graphics.GAL/Capabilities.cs
index bc4a02c9..a93d3846 100644
--- a/Ryujinx.Graphics.GAL/Capabilities.cs
+++ b/Ryujinx.Graphics.GAL/Capabilities.cs
@@ -35,7 +35,8 @@ namespace Ryujinx.Graphics.GAL
         public readonly bool SupportsNonConstantTextureOffset;
         public readonly bool SupportsShaderBallot;
         public readonly bool SupportsTextureShadowLod;
-        public readonly bool SupportsViewportIndex;
+        public readonly bool SupportsViewportIndexVertexTessellation;
+        public readonly bool SupportsViewportMask;
         public readonly bool SupportsViewportSwizzle;
         public readonly bool SupportsIndirectParameters;
 
@@ -80,7 +81,8 @@ namespace Ryujinx.Graphics.GAL
             bool supportsNonConstantTextureOffset,
             bool supportsShaderBallot,
             bool supportsTextureShadowLod,
-            bool supportsViewportIndex,
+            bool supportsViewportIndexVertexTessellation,
+            bool supportsViewportMask,
             bool supportsViewportSwizzle,
             bool supportsIndirectParameters,
             uint maximumUniformBuffersPerStage,
@@ -121,7 +123,8 @@ namespace Ryujinx.Graphics.GAL
             SupportsNonConstantTextureOffset = supportsNonConstantTextureOffset;
             SupportsShaderBallot = supportsShaderBallot;
             SupportsTextureShadowLod = supportsTextureShadowLod;
-            SupportsViewportIndex = supportsViewportIndex;
+            SupportsViewportIndexVertexTessellation = supportsViewportIndexVertexTessellation;
+            SupportsViewportMask = supportsViewportMask;
             SupportsViewportSwizzle = supportsViewportSwizzle;
             SupportsIndirectParameters = supportsIndirectParameters;
             MaximumUniformBuffersPerStage = maximumUniformBuffersPerStage;
diff --git a/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs b/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs
index cad2341b..78f9763f 100644
--- a/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs
+++ b/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 = 4707;
+        private const uint CodeGenVersion = 4565;
 
         private const string SharedTocFileName = "shared.toc";
         private const string SharedDataFileName = "shared.data";
diff --git a/Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs b/Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs
index bbf2702e..d35b8d92 100644
--- a/Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs
+++ b/Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs
@@ -144,7 +144,9 @@ namespace Ryujinx.Graphics.Gpu.Shader
 
         public bool QueryHostSupportsTextureShadowLod() => _context.Capabilities.SupportsTextureShadowLod;
 
-        public bool QueryHostSupportsViewportIndex() => _context.Capabilities.SupportsViewportIndex;
+        public bool QueryHostSupportsViewportIndexVertexTessellation() => _context.Capabilities.SupportsViewportIndexVertexTessellation;
+
+        public bool QueryHostSupportsViewportMask() => _context.Capabilities.SupportsViewportMask;
 
         /// <summary>
         /// Converts a packed Maxwell texture format to the shader translator texture format.
diff --git a/Ryujinx.Graphics.OpenGL/HwCapabilities.cs b/Ryujinx.Graphics.OpenGL/HwCapabilities.cs
index 84646526..bf365b4d 100644
--- a/Ryujinx.Graphics.OpenGL/HwCapabilities.cs
+++ b/Ryujinx.Graphics.OpenGL/HwCapabilities.cs
@@ -20,6 +20,7 @@ namespace Ryujinx.Graphics.OpenGL
         private static readonly Lazy<bool> _supportsSeamlessCubemapPerTexture    = new Lazy<bool>(() => HasExtension("GL_ARB_seamless_cubemap_per_texture"));
         private static readonly Lazy<bool> _supportsShaderBallot                 = new Lazy<bool>(() => HasExtension("GL_ARB_shader_ballot"));
         private static readonly Lazy<bool> _supportsShaderViewportLayerArray     = new Lazy<bool>(() => HasExtension("GL_ARB_shader_viewport_layer_array"));
+        private static readonly Lazy<bool> _supportsViewportArray2               = new Lazy<bool>(() => HasExtension("GL_NV_viewport_array2"));
         private static readonly Lazy<bool> _supportsTextureCompressionBptc       = new Lazy<bool>(() => HasExtension("GL_EXT_texture_compression_bptc"));
         private static readonly Lazy<bool> _supportsTextureCompressionRgtc       = new Lazy<bool>(() => HasExtension("GL_EXT_texture_compression_rgtc"));
         private static readonly Lazy<bool> _supportsTextureCompressionS3tc       = new Lazy<bool>(() => HasExtension("GL_EXT_texture_compression_s3tc"));
@@ -65,6 +66,7 @@ namespace Ryujinx.Graphics.OpenGL
         public static bool SupportsSeamlessCubemapPerTexture    => _supportsSeamlessCubemapPerTexture.Value;
         public static bool SupportsShaderBallot                 => _supportsShaderBallot.Value;
         public static bool SupportsShaderViewportLayerArray     => _supportsShaderViewportLayerArray.Value;
+        public static bool SupportsViewportArray2               => _supportsViewportArray2.Value;
         public static bool SupportsTextureCompressionBptc       => _supportsTextureCompressionBptc.Value;
         public static bool SupportsTextureCompressionRgtc       => _supportsTextureCompressionRgtc.Value;
         public static bool SupportsTextureCompressionS3tc       => _supportsTextureCompressionS3tc.Value;
diff --git a/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs b/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs
index 5a2e3fe4..3903b4d4 100644
--- a/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs
+++ b/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs
@@ -136,7 +136,8 @@ namespace Ryujinx.Graphics.OpenGL
                 supportsNonConstantTextureOffset: HwCapabilities.SupportsNonConstantTextureOffset,
                 supportsShaderBallot: HwCapabilities.SupportsShaderBallot,
                 supportsTextureShadowLod: HwCapabilities.SupportsTextureShadowLod,
-                supportsViewportIndex: HwCapabilities.SupportsShaderViewportLayerArray,
+                supportsViewportIndexVertexTessellation: HwCapabilities.SupportsShaderViewportLayerArray,
+                supportsViewportMask: HwCapabilities.SupportsViewportArray2,
                 supportsViewportSwizzle: HwCapabilities.SupportsViewportSwizzle,
                 supportsIndirectParameters: HwCapabilities.SupportsIndirectParameters,
                 maximumUniformBuffersPerStage: 13, // TODO: Avoid hardcoding those limits here and get from driver?
diff --git a/Ryujinx.Graphics.Shader/CodeGen/Glsl/Declarations.cs b/Ryujinx.Graphics.Shader/CodeGen/Glsl/Declarations.cs
index 5e53d62a..81b79ec4 100644
--- a/Ryujinx.Graphics.Shader/CodeGen/Glsl/Declarations.cs
+++ b/Ryujinx.Graphics.Shader/CodeGen/Glsl/Declarations.cs
@@ -59,6 +59,11 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
                 context.AppendLine("#extension GL_NV_geometry_shader_passthrough : enable");
             }
 
+            if (context.Config.GpuAccessor.QueryHostSupportsViewportMask())
+            {
+                context.AppendLine("#extension GL_NV_viewport_array2 : enable");
+            }
+
             context.AppendLine("#pragma optionNV(fastmath off)");
             context.AppendLine();
 
@@ -215,7 +220,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
 
                 if (context.Config.TransformFeedbackEnabled && context.Config.LastInVertexPipeline)
                 {
-                    var tfOutput = context.Info.GetTransformFeedbackOutput(AttributeConsts.PositionX);
+                    var tfOutput = context.Config.GetTransformFeedbackOutput(AttributeConsts.PositionX);
                     if (tfOutput.Valid)
                     {
                         context.AppendLine($"layout (xfb_buffer = {tfOutput.Buffer}, xfb_offset = {tfOutput.Offset}, xfb_stride = {tfOutput.Stride}) out gl_PerVertex");
@@ -552,7 +557,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
 
         private static void DeclareInputAttribute(CodeGenContext context, StructuredProgramInfo info, int attr)
         {
-            string suffix = AttributeInfo.IsArrayAttributeGlsl(context.Config.Stage, isOutAttr: false) ? "[]" : string.Empty;
+            string suffix = IsArrayAttributeGlsl(context.Config.Stage, isOutAttr: false) ? "[]" : string.Empty;
             string iq = string.Empty;
 
             if (context.Config.Stage == ShaderStage.Fragment)
@@ -569,8 +574,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
 
             if (context.Config.TransformFeedbackEnabled && context.Config.Stage == ShaderStage.Fragment)
             {
-                int attrOffset = AttributeConsts.UserAttributeBase + attr * 16;
-                int components = context.Info.GetTransformFeedbackOutputComponents(attrOffset);
+                int components = context.Config.GetTransformFeedbackOutputComponents(attr, 0);
 
                 if (components > 1)
                 {
@@ -652,13 +656,12 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
 
         private static void DeclareOutputAttribute(CodeGenContext context, int attr)
         {
-            string suffix = AttributeInfo.IsArrayAttributeGlsl(context.Config.Stage, isOutAttr: true) ? "[]" : string.Empty;
+            string suffix = IsArrayAttributeGlsl(context.Config.Stage, isOutAttr: true) ? "[]" : string.Empty;
             string name = $"{DefaultNames.OAttributePrefix}{attr}{suffix}";
 
             if (context.Config.TransformFeedbackEnabled && context.Config.LastInVertexPipeline)
             {
-                int attrOffset = AttributeConsts.UserAttributeBase + attr * 16;
-                int components = context.Info.GetTransformFeedbackOutputComponents(attrOffset);
+                int components = context.Config.GetTransformFeedbackOutputComponents(attr, 0);
 
                 if (components > 1)
                 {
@@ -672,7 +675,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
 
                     string xfb = string.Empty;
 
-                    var tfOutput = context.Info.GetTransformFeedbackOutput(attrOffset);
+                    var tfOutput = context.Config.GetTransformFeedbackOutput(attr, 0);
                     if (tfOutput.Valid)
                     {
                         xfb = $", xfb_buffer = {tfOutput.Buffer}, xfb_offset = {tfOutput.Offset}, xfb_stride = {tfOutput.Stride}";
@@ -687,7 +690,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
 
                     string xfb = string.Empty;
 
-                    var tfOutput = context.Info.GetTransformFeedbackOutput(attrOffset + c * 4);
+                    var tfOutput = context.Config.GetTransformFeedbackOutput(attr, c);
                     if (tfOutput.Valid)
                     {
                         xfb = $", xfb_buffer = {tfOutput.Buffer}, xfb_offset = {tfOutput.Offset}, xfb_stride = {tfOutput.Stride}";
@@ -726,6 +729,20 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
             context.AppendLine($"layout (location = {attr}, index = 1) out vec4 {name2};");
         }
 
+        private static bool IsArrayAttributeGlsl(ShaderStage stage, bool isOutAttr)
+        {
+            if (isOutAttr)
+            {
+                return stage == ShaderStage.TessellationControl;
+            }
+            else
+            {
+                return stage == ShaderStage.TessellationControl ||
+                       stage == ShaderStage.TessellationEvaluation ||
+                       stage == ShaderStage.Geometry;
+            }
+        }
+
         private static void DeclareUsedOutputAttributesPerPatch(CodeGenContext context, HashSet<int> attrs)
         {
             foreach (int attr in attrs.Order())
diff --git a/Ryujinx.Graphics.Shader/CodeGen/Glsl/GlslGenerator.cs b/Ryujinx.Graphics.Shader/CodeGen/Glsl/GlslGenerator.cs
index 90727558..751d0350 100644
--- a/Ryujinx.Graphics.Shader/CodeGen/Glsl/GlslGenerator.cs
+++ b/Ryujinx.Graphics.Shader/CodeGen/Glsl/GlslGenerator.cs
@@ -1,5 +1,4 @@
 using Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions;
-using Ryujinx.Graphics.Shader.IntermediateRepresentation;
 using Ryujinx.Graphics.Shader.StructuredIr;
 using Ryujinx.Graphics.Shader.Translation;
 using System;
@@ -126,21 +125,10 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
                 }
                 else if (node is AstAssignment assignment)
                 {
+                    AggregateType dstType = OperandManager.GetNodeDestType(context, assignment.Destination);
                     AggregateType srcType = OperandManager.GetNodeDestType(context, assignment.Source);
-                    AggregateType dstType = OperandManager.GetNodeDestType(context, assignment.Destination, isAsgDest: true);
-
-                    string dest;
-
-                    if (assignment.Destination is AstOperand operand && operand.Type.IsAttribute())
-                    {
-                        bool perPatch = operand.Type == OperandType.AttributePerPatch;
-                        dest = OperandManager.GetOutAttributeName(context, operand.Value, perPatch);
-                    }
-                    else
-                    {
-                        dest = InstGen.GetExpression(context, assignment.Destination);
-                    }
 
+                    string dest = InstGen.GetExpression(context, assignment.Destination);
                     string src = ReinterpretCast(context, assignment.Source, srcType, dstType);
 
                     context.AppendLine(dest + " = " + src + ";");
diff --git a/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGen.cs b/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGen.cs
index 9ca4618d..01bd11e5 100644
--- a/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGen.cs
+++ b/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGen.cs
@@ -73,7 +73,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
                     // For shared memory access, the second argument is unused and should be ignored.
                     // It is there to make both storage and shared access have the same number of arguments.
                     // For storage, both inputs are consumed when the argument index is 0, so we should skip it here.
-                    if (argIndex == 1 && (atomic || (inst & Instruction.MrMask) == Instruction.MrShared))
+                    if (argIndex == 1 && (atomic || operation.StorageKind == StorageKind.SharedMemory))
                     {
                         continue;
                     }
@@ -85,14 +85,12 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
 
                     if (argIndex == 0 && atomic)
                     {
-                        Instruction memRegion = inst & Instruction.MrMask;
-
-                        switch (memRegion)
+                        switch (operation.StorageKind)
                         {
-                            case Instruction.MrShared: args += LoadShared(context, operation); break;
-                            case Instruction.MrStorage: args += LoadStorage(context, operation); break;
+                            case StorageKind.SharedMemory: args += LoadShared(context, operation); break;
+                            case StorageKind.StorageBuffer: args += LoadStorage(context, operation); break;
 
-                            default: throw new InvalidOperationException($"Invalid memory region \"{memRegion}\".");
+                            default: throw new InvalidOperationException($"Invalid storage kind \"{operation.StorageKind}\".");
                         }
                     }
                     else
@@ -166,8 +164,8 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
                     case Instruction.ImageAtomic:
                         return ImageLoadOrStore(context, operation);
 
-                    case Instruction.LoadAttribute:
-                        return LoadAttribute(context, operation);
+                    case Instruction.Load:
+                        return Load(context, operation);
 
                     case Instruction.LoadConstant:
                         return LoadConstant(context, operation);
@@ -193,8 +191,8 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
                     case Instruction.PackHalf2x16:
                         return PackHalf2x16(context, operation);
 
-                    case Instruction.StoreAttribute:
-                        return StoreAttribute(context, operation);
+                    case Instruction.Store:
+                        return Store(context, operation);
 
                     case Instruction.StoreLocal:
                         return StoreLocal(context, operation);
diff --git a/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenHelper.cs b/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenHelper.cs
index 743b695c..00478f6a 100644
--- a/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenHelper.cs
+++ b/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenHelper.cs
@@ -82,7 +82,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
             Add(Instruction.ImageStore,               InstType.Special);
             Add(Instruction.ImageAtomic,              InstType.Special);
             Add(Instruction.IsNan,                    InstType.CallUnary,      "isnan");
-            Add(Instruction.LoadAttribute,            InstType.Special);
+            Add(Instruction.Load,                     InstType.Special);
             Add(Instruction.LoadConstant,             InstType.Special);
             Add(Instruction.LoadLocal,                InstType.Special);
             Add(Instruction.LoadShared,               InstType.Special);
@@ -118,7 +118,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
             Add(Instruction.ShuffleXor,               InstType.CallQuaternary, HelperFunctionNames.ShuffleXor);
             Add(Instruction.Sine,                     InstType.CallUnary,      "sin");
             Add(Instruction.SquareRoot,               InstType.CallUnary,      "sqrt");
-            Add(Instruction.StoreAttribute,           InstType.Special);
+            Add(Instruction.Store,                    InstType.Special);
             Add(Instruction.StoreLocal,               InstType.Special);
             Add(Instruction.StoreShared,              InstType.Special);
             Add(Instruction.StoreShared16,            InstType.Special);
diff --git a/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenMemory.cs b/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenMemory.cs
index a5d2632c..99519837 100644
--- a/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenMemory.cs
+++ b/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenMemory.cs
@@ -210,30 +210,9 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
             return texCallBuilder.ToString();
         }
 
-        public static string LoadAttribute(CodeGenContext context, AstOperation operation)
+        public static string Load(CodeGenContext context, AstOperation operation)
         {
-            IAstNode src1 = operation.GetSource(0);
-            IAstNode src2 = operation.GetSource(1);
-            IAstNode src3 = operation.GetSource(2);
-
-            if (!(src1 is AstOperand baseAttr) || baseAttr.Type != OperandType.Constant)
-            {
-                throw new InvalidOperationException($"First input of {nameof(Instruction.LoadAttribute)} must be a constant operand.");
-            }
-
-            string indexExpr = GetSoureExpr(context, src3, GetSrcVarType(operation.Inst, 2));
-
-            if (src2 is AstOperand operand && operand.Type == OperandType.Constant)
-            {
-                int attrOffset = baseAttr.Value + (operand.Value << 2);
-                return OperandManager.GetAttributeName(context, attrOffset, perPatch: false, isOutAttr: false, indexExpr);
-            }
-            else
-            {
-                string attrExpr = GetSoureExpr(context, src2, GetSrcVarType(operation.Inst, 1));
-                attrExpr = Enclose(attrExpr, src2, Instruction.ShiftRightS32, isLhs: true);
-                return OperandManager.GetAttributeName(attrExpr, context.Config, isOutAttr: false, indexExpr);
-            }
+            return GenerateLoadOrStore(context, operation, isStore: false);
         }
 
         public static string LoadConstant(CodeGenContext context, AstOperation operation)
@@ -337,33 +316,9 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
             return $"textureQueryLod({samplerName}, {coordsExpr}){GetMask(texOp.Index)}";
         }
 
-        public static string StoreAttribute(CodeGenContext context, AstOperation operation)
+        public static string Store(CodeGenContext context, AstOperation operation)
         {
-            IAstNode src1 = operation.GetSource(0);
-            IAstNode src2 = operation.GetSource(1);
-            IAstNode src3 = operation.GetSource(2);
-
-            if (!(src1 is AstOperand baseAttr) || baseAttr.Type != OperandType.Constant)
-            {
-                throw new InvalidOperationException($"First input of {nameof(Instruction.StoreAttribute)} must be a constant operand.");
-            }
-
-            string attrName;
-
-            if (src2 is AstOperand operand && operand.Type == OperandType.Constant)
-            {
-                int attrOffset = baseAttr.Value + (operand.Value << 2);
-                attrName = OperandManager.GetAttributeName(context, attrOffset, perPatch: false, isOutAttr: true);
-            }
-            else
-            {
-                string attrExpr = GetSoureExpr(context, src2, GetSrcVarType(operation.Inst, 1));
-                attrExpr = Enclose(attrExpr, src2, Instruction.ShiftRightS32, isLhs: true);
-                attrName = OperandManager.GetAttributeName(attrExpr, context.Config, isOutAttr: true);
-            }
-
-            string value = GetSoureExpr(context, src3, GetSrcVarType(operation.Inst, 2));
-            return $"{attrName} = {value}";
+            return GenerateLoadOrStore(context, operation, isStore: true);
         }
 
         public static string StoreLocal(CodeGenContext context, AstOperation operation)
@@ -847,6 +802,111 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
             }
         }
 
+        private static string GenerateLoadOrStore(CodeGenContext context, AstOperation operation, bool isStore)
+        {
+            StorageKind storageKind = operation.StorageKind;
+
+            string varName;
+            AggregateType varType;
+            int srcIndex = 0;
+
+            switch (storageKind)
+            {
+                case StorageKind.Input:
+                case StorageKind.InputPerPatch:
+                case StorageKind.Output:
+                case StorageKind.OutputPerPatch:
+                    if (!(operation.GetSource(srcIndex++) is AstOperand varId) || varId.Type != OperandType.Constant)
+                    {
+                        throw new InvalidOperationException($"First input of {operation.Inst} with {storageKind} storage must be a constant operand.");
+                    }
+
+                    IoVariable ioVariable = (IoVariable)varId.Value;
+                    bool isOutput = storageKind.IsOutput();
+                    bool isPerPatch = storageKind.IsPerPatch();
+                    int location = -1;
+                    int component = 0;
+
+                    if (context.Config.HasPerLocationInputOrOutput(ioVariable, isOutput))
+                    {
+                        if (!(operation.GetSource(srcIndex++) is AstOperand vecIndex) || vecIndex.Type != OperandType.Constant)
+                        {
+                            throw new InvalidOperationException($"Second input of {operation.Inst} with {storageKind} storage must be a constant operand.");
+                        }
+
+                        location = vecIndex.Value;
+
+                        if (operation.SourcesCount > srcIndex &&
+                            operation.GetSource(srcIndex) is AstOperand elemIndex &&
+                            elemIndex.Type == OperandType.Constant &&
+                            context.Config.HasPerLocationInputOrOutputComponent(ioVariable, location, elemIndex.Value, isOutput))
+                        {
+                            component = elemIndex.Value;
+                            srcIndex++;
+                        }
+                    }
+
+                    (varName, varType) = IoMap.GetGlslVariable(context.Config, ioVariable, location, component, isOutput, isPerPatch);
+
+                    if (IoMap.IsPerVertexBuiltIn(context.Config.Stage, ioVariable, isOutput))
+                    {
+                        // Since those exist both as input and output on geometry and tessellation shaders,
+                        // we need the gl_in and gl_out prefixes to disambiguate.
+
+                        if (storageKind == StorageKind.Input)
+                        {
+                            string expr = GetSoureExpr(context, operation.GetSource(srcIndex++), AggregateType.S32);
+                            varName = $"gl_in[{expr}].{varName}";
+                        }
+                        else if (storageKind == StorageKind.Output)
+                        {
+                            string expr = GetSoureExpr(context, operation.GetSource(srcIndex++), AggregateType.S32);
+                            varName = $"gl_out[{expr}].{varName}";
+                        }
+                    }
+
+                    int firstSrcIndex = srcIndex;
+                    int inputsCount = isStore ? operation.SourcesCount - 1 : operation.SourcesCount;
+
+                    for (; srcIndex < inputsCount; srcIndex++)
+                    {
+                        IAstNode src = operation.GetSource(srcIndex);
+
+                        if ((varType & AggregateType.ElementCountMask) != 0 &&
+                            srcIndex == inputsCount - 1 &&
+                            src is AstOperand elementIndex &&
+                            elementIndex.Type == OperandType.Constant)
+                        {
+                            varName += "." + "xyzw"[elementIndex.Value & 3];
+                        }
+                        else if (srcIndex == firstSrcIndex && context.Config.Stage == ShaderStage.TessellationControl && storageKind == StorageKind.Output)
+                        {
+                            // GLSL requires that for tessellation control shader outputs,
+                            // that the index expression must be *exactly* "gl_InvocationID",
+                            // otherwise the compilation fails.
+                            // TODO: Get rid of this and use expression propagation to make sure we generate the correct code from IR.
+                            varName += "[gl_InvocationID]";
+                        }
+                        else
+                        {
+                            varName += $"[{GetSoureExpr(context, src, AggregateType.S32)}]";
+                        }
+                    }
+                    break;
+
+                default:
+                    throw new InvalidOperationException($"Invalid storage kind {storageKind}.");
+            }
+
+            if (isStore)
+            {
+                varType &= AggregateType.ElementTypeMask;
+                varName = $"{varName} = {GetSoureExpr(context, operation.GetSource(srcIndex), varType)}";
+            }
+
+            return varName;
+        }
+
         private static string GetStorageBufferAccessor(string slotExpr, string offsetExpr, ShaderStage stage)
         {
             string sbName = OperandManager.GetShaderStagePrefix(stage);
diff --git a/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/IoMap.cs b/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/IoMap.cs
new file mode 100644
index 00000000..093ee232
--- /dev/null
+++ b/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/IoMap.cs
@@ -0,0 +1,145 @@
+using Ryujinx.Graphics.Shader.IntermediateRepresentation;
+using Ryujinx.Graphics.Shader.Translation;
+using System.Globalization;
+
+namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
+{
+    static class IoMap
+    {
+        public static (string, AggregateType) GetGlslVariable(
+            ShaderConfig config,
+            IoVariable ioVariable,
+            int location,
+            int component,
+            bool isOutput,
+            bool isPerPatch)
+        {
+            return ioVariable switch
+            {
+                IoVariable.BackColorDiffuse => ("gl_BackColor", AggregateType.Vector4 | AggregateType.FP32), // Deprecated.
+                IoVariable.BackColorSpecular => ("gl_BackSecondaryColor", AggregateType.Vector4 | AggregateType.FP32), // Deprecated.
+                IoVariable.BaseInstance => ("gl_BaseInstanceARB", AggregateType.S32),
+                IoVariable.BaseVertex => ("gl_BaseVertexARB", AggregateType.S32),
+                IoVariable.ClipDistance => ("gl_ClipDistance", AggregateType.Array | AggregateType.FP32),
+                IoVariable.CtaId => ("gl_WorkGroupID", AggregateType.Vector3 | AggregateType.U32),
+                IoVariable.DrawIndex => ("gl_DrawIDARB", AggregateType.S32),
+                IoVariable.FogCoord => ("gl_FogFragCoord", AggregateType.FP32), // Deprecated.
+                IoVariable.FragmentCoord => ("gl_FragCoord", AggregateType.Vector4 | AggregateType.FP32),
+                IoVariable.FragmentOutputColor => GetFragmentOutputColorVariableName(config, location),
+                IoVariable.FragmentOutputDepth => ("gl_FragDepth", AggregateType.FP32),
+                IoVariable.FragmentOutputIsBgra => (DefaultNames.SupportBlockIsBgraName, AggregateType.Array | AggregateType.Bool),
+                IoVariable.FrontColorDiffuse => ("gl_FrontColor", AggregateType.Vector4 | AggregateType.FP32), // Deprecated.
+                IoVariable.FrontColorSpecular  => ("gl_FrontSecondaryColor", AggregateType.Vector4 | AggregateType.FP32), // Deprecated.
+                IoVariable.FrontFacing => ("gl_FrontFacing", AggregateType.Bool),
+                IoVariable.InstanceId => ("gl_InstanceID", AggregateType.S32),
+                IoVariable.InstanceIndex => ("gl_InstanceIndex", AggregateType.S32),
+                IoVariable.InvocationId => ("gl_InvocationID", AggregateType.S32),
+                IoVariable.Layer => ("gl_Layer", AggregateType.S32),
+                IoVariable.PatchVertices => ("gl_PatchVerticesIn", AggregateType.S32),
+                IoVariable.PointCoord => ("gl_PointCoord", AggregateType.Vector2 | AggregateType.FP32),
+                IoVariable.PointSize => ("gl_PointSize", AggregateType.FP32),
+                IoVariable.Position => ("gl_Position", AggregateType.Vector4 | AggregateType.FP32),
+                IoVariable.PrimitiveId => GetPrimitiveIdVariableName(config.Stage, isOutput),
+                IoVariable.SubgroupEqMask => GetSubgroupMaskVariableName(config, "Eq"),
+                IoVariable.SubgroupGeMask => GetSubgroupMaskVariableName(config, "Ge"),
+                IoVariable.SubgroupGtMask => GetSubgroupMaskVariableName(config, "Gt"),
+                IoVariable.SubgroupLaneId => GetSubgroupInvocationIdVariableName(config),
+                IoVariable.SubgroupLeMask => GetSubgroupMaskVariableName(config, "Le"),
+                IoVariable.SubgroupLtMask => GetSubgroupMaskVariableName(config, "Lt"),
+                IoVariable.SupportBlockRenderScale => (DefaultNames.SupportBlockRenderScaleName, AggregateType.Array | AggregateType.FP32),
+                IoVariable.SupportBlockViewInverse => (DefaultNames.SupportBlockViewportInverse, AggregateType.Vector2 | AggregateType.FP32),
+                IoVariable.TessellationCoord => ("gl_TessCoord", AggregateType.Vector3 | AggregateType.FP32),
+                IoVariable.TessellationLevelInner => ("gl_TessLevelInner", AggregateType.Array | AggregateType.FP32),
+                IoVariable.TessellationLevelOuter => ("gl_TessLevelOuter", AggregateType.Array | AggregateType.FP32),
+                IoVariable.TextureCoord => ("gl_TexCoord", AggregateType.Array | AggregateType.Vector4 | AggregateType.FP32), // Deprecated.
+                IoVariable.ThreadId => ("gl_LocalInvocationID", AggregateType.Vector3 | AggregateType.U32),
+                IoVariable.ThreadKill => ("gl_HelperInvocation", AggregateType.Bool),
+                IoVariable.UserDefined => GetUserDefinedVariableName(config, location, component, isOutput, isPerPatch),
+                IoVariable.VertexId => ("gl_VertexID", AggregateType.S32),
+                IoVariable.VertexIndex => ("gl_VertexIndex", AggregateType.S32),
+                IoVariable.ViewportIndex => ("gl_ViewportIndex", AggregateType.S32),
+                IoVariable.ViewportMask => ("gl_ViewportMask", AggregateType.Array | AggregateType.S32),
+                _ => (null, AggregateType.Invalid)
+            };
+        }
+
+        public static bool IsPerVertexBuiltIn(ShaderStage stage, IoVariable ioVariable, bool isOutput)
+        {
+            switch (ioVariable)
+            {
+                case IoVariable.Layer:
+                case IoVariable.ViewportIndex:
+                case IoVariable.PointSize:
+                case IoVariable.Position:
+                case IoVariable.ClipDistance:
+                case IoVariable.PointCoord:
+                case IoVariable.ViewportMask:
+                    if (isOutput)
+                    {
+                        return stage == ShaderStage.TessellationControl;
+                    }
+                    else
+                    {
+                        return stage == ShaderStage.TessellationControl ||
+                               stage == ShaderStage.TessellationEvaluation ||
+                               stage == ShaderStage.Geometry;
+                    }
+            }
+
+            return false;
+        }
+
+        private static (string, AggregateType) GetFragmentOutputColorVariableName(ShaderConfig config, int location)
+        {
+            if (location < 0)
+            {
+                return (DefaultNames.OAttributePrefix, config.GetFragmentOutputColorType(0));
+            }
+
+            string name = DefaultNames.OAttributePrefix + location.ToString(CultureInfo.InvariantCulture);
+
+            return (name, config.GetFragmentOutputColorType(location));
+        }
+
+        private static (string, AggregateType) GetPrimitiveIdVariableName(ShaderStage stage, bool isOutput)
+        {
+            // The geometry stage has an additional gl_PrimitiveIDIn variable.
+            return (isOutput || stage != ShaderStage.Geometry ? "gl_PrimitiveID" : "gl_PrimitiveIDIn", AggregateType.S32);
+        }
+
+        private static (string, AggregateType) GetSubgroupMaskVariableName(ShaderConfig config, string cc)
+        {
+            return config.GpuAccessor.QueryHostSupportsShaderBallot()
+                ? ($"unpackUint2x32(gl_SubGroup{cc}MaskARB)", AggregateType.Vector2 | AggregateType.U32)
+                : ($"gl_Subgroup{cc}Mask", AggregateType.Vector4 | AggregateType.U32);
+        }
+
+        private static (string, AggregateType) GetSubgroupInvocationIdVariableName(ShaderConfig config)
+        {
+            return config.GpuAccessor.QueryHostSupportsShaderBallot()
+                ? ("gl_SubGroupInvocationARB", AggregateType.U32)
+                : ("gl_SubgroupInvocationID", AggregateType.U32);
+        }
+
+        private static (string, AggregateType) GetUserDefinedVariableName(ShaderConfig config, int location, int component, bool isOutput, bool isPerPatch)
+        {
+            string name = isPerPatch
+                ? DefaultNames.PerPatchAttributePrefix
+                : (isOutput ? DefaultNames.OAttributePrefix : DefaultNames.IAttributePrefix);
+
+            if (location < 0)
+            {
+                return (name, config.GetUserDefinedType(0, isOutput));
+            }
+
+            name += location.ToString(CultureInfo.InvariantCulture);
+
+            if (config.HasPerLocationInputOrOutputComponent(IoVariable.UserDefined, location, component, isOutput))
+            {
+                name += "_" + "xyzw"[component & 3];
+            }
+
+            return (name, config.GetUserDefinedType(location, isOutput));
+        }
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.Graphics.Shader/CodeGen/Glsl/OperandManager.cs b/Ryujinx.Graphics.Shader/CodeGen/Glsl/OperandManager.cs
index ec761fa6..92e83358 100644
--- a/Ryujinx.Graphics.Shader/CodeGen/Glsl/OperandManager.cs
+++ b/Ryujinx.Graphics.Shader/CodeGen/Glsl/OperandManager.cs
@@ -1,10 +1,10 @@
+using Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions;
 using Ryujinx.Graphics.Shader.IntermediateRepresentation;
 using Ryujinx.Graphics.Shader.StructuredIr;
 using Ryujinx.Graphics.Shader.Translation;
 using System;
 using System.Collections.Generic;
 using System.Diagnostics;
-using System.Numerics;
 
 using static Ryujinx.Graphics.Shader.StructuredIr.InstructionInfo;
 
@@ -12,82 +12,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
 {
     class OperandManager
     {
-        private static readonly string[] StagePrefixes = new string[] { "cp", "vp", "tcp", "tep", "gp", "fp" };
-
-        private readonly struct BuiltInAttribute
-        {
-            public string Name { get; }
-
-            public AggregateType Type { get; }
-
-            public BuiltInAttribute(string name, AggregateType type)
-            {
-                Name = name;
-                Type = type;
-            }
-        }
-
-        private static Dictionary<int, BuiltInAttribute> _builtInAttributes = new Dictionary<int, BuiltInAttribute>()
-        {
-            { AttributeConsts.Layer,         new BuiltInAttribute("gl_Layer",           AggregateType.S32)  },
-            { AttributeConsts.PointSize,     new BuiltInAttribute("gl_PointSize",       AggregateType.FP32)  },
-            { AttributeConsts.PositionX,     new BuiltInAttribute("gl_Position.x",      AggregateType.FP32)  },
-            { AttributeConsts.PositionY,     new BuiltInAttribute("gl_Position.y",      AggregateType.FP32)  },
-            { AttributeConsts.PositionZ,     new BuiltInAttribute("gl_Position.z",      AggregateType.FP32)  },
-            { AttributeConsts.PositionW,     new BuiltInAttribute("gl_Position.w",      AggregateType.FP32)  },
-            { AttributeConsts.ClipDistance0, new BuiltInAttribute("gl_ClipDistance[0]", AggregateType.FP32)  },
-            { AttributeConsts.ClipDistance1, new BuiltInAttribute("gl_ClipDistance[1]", AggregateType.FP32)  },
-            { AttributeConsts.ClipDistance2, new BuiltInAttribute("gl_ClipDistance[2]", AggregateType.FP32)  },
-            { AttributeConsts.ClipDistance3, new BuiltInAttribute("gl_ClipDistance[3]", AggregateType.FP32)  },
-            { AttributeConsts.ClipDistance4, new BuiltInAttribute("gl_ClipDistance[4]", AggregateType.FP32)  },
-            { AttributeConsts.ClipDistance5, new BuiltInAttribute("gl_ClipDistance[5]", AggregateType.FP32)  },
-            { AttributeConsts.ClipDistance6, new BuiltInAttribute("gl_ClipDistance[6]", AggregateType.FP32)  },
-            { AttributeConsts.ClipDistance7, new BuiltInAttribute("gl_ClipDistance[7]", AggregateType.FP32)  },
-            { AttributeConsts.PointCoordX,   new BuiltInAttribute("gl_PointCoord.x",    AggregateType.FP32)  },
-            { AttributeConsts.PointCoordY,   new BuiltInAttribute("gl_PointCoord.y",    AggregateType.FP32)  },
-            { AttributeConsts.TessCoordX,    new BuiltInAttribute("gl_TessCoord.x",     AggregateType.FP32)  },
-            { AttributeConsts.TessCoordY,    new BuiltInAttribute("gl_TessCoord.y",     AggregateType.FP32)  },
-            { AttributeConsts.InstanceId,    new BuiltInAttribute("gl_InstanceID",      AggregateType.S32)  },
-            { AttributeConsts.VertexId,      new BuiltInAttribute("gl_VertexID",        AggregateType.S32)  },
-            { AttributeConsts.BaseInstance,  new BuiltInAttribute("gl_BaseInstanceARB", AggregateType.S32)  },
-            { AttributeConsts.BaseVertex,    new BuiltInAttribute("gl_BaseVertexARB",   AggregateType.S32)  },
-            { AttributeConsts.InstanceIndex, new BuiltInAttribute("gl_InstanceIndex",   AggregateType.S32)  },
-            { AttributeConsts.VertexIndex,   new BuiltInAttribute("gl_VertexIndex",     AggregateType.S32)  },
-            { AttributeConsts.DrawIndex,     new BuiltInAttribute("gl_DrawIDARB",       AggregateType.S32)  },
-            { AttributeConsts.FrontFacing,   new BuiltInAttribute("gl_FrontFacing",     AggregateType.Bool) },
-
-            // Special.
-            { AttributeConsts.FragmentOutputDepth, new BuiltInAttribute("gl_FragDepth",           AggregateType.FP32)  },
-            { AttributeConsts.ThreadKill,          new BuiltInAttribute("gl_HelperInvocation",    AggregateType.Bool) },
-            { AttributeConsts.ThreadIdX,           new BuiltInAttribute("gl_LocalInvocationID.x", AggregateType.U32)  },
-            { AttributeConsts.ThreadIdY,           new BuiltInAttribute("gl_LocalInvocationID.y", AggregateType.U32)  },
-            { AttributeConsts.ThreadIdZ,           new BuiltInAttribute("gl_LocalInvocationID.z", AggregateType.U32)  },
-            { AttributeConsts.CtaIdX,              new BuiltInAttribute("gl_WorkGroupID.x",       AggregateType.U32)  },
-            { AttributeConsts.CtaIdY,              new BuiltInAttribute("gl_WorkGroupID.y",       AggregateType.U32)  },
-            { AttributeConsts.CtaIdZ,              new BuiltInAttribute("gl_WorkGroupID.z",       AggregateType.U32)  },
-            { AttributeConsts.LaneId,              new BuiltInAttribute(null,                     AggregateType.U32)  },
-            { AttributeConsts.InvocationId,        new BuiltInAttribute("gl_InvocationID",        AggregateType.S32)  },
-            { AttributeConsts.PrimitiveId,         new BuiltInAttribute("gl_PrimitiveID",         AggregateType.S32)  },
-            { AttributeConsts.PatchVerticesIn,     new BuiltInAttribute("gl_PatchVerticesIn",     AggregateType.S32)  },
-            { AttributeConsts.EqMask,              new BuiltInAttribute(null,                     AggregateType.U32)  },
-            { AttributeConsts.GeMask,              new BuiltInAttribute(null,                     AggregateType.U32)  },
-            { AttributeConsts.GtMask,              new BuiltInAttribute(null,                     AggregateType.U32)  },
-            { AttributeConsts.LeMask,              new BuiltInAttribute(null,                     AggregateType.U32)  },
-            { AttributeConsts.LtMask,              new BuiltInAttribute(null,                     AggregateType.U32)  },
-
-            // Support uniforms.
-            { AttributeConsts.FragmentOutputIsBgraBase + 0,  new BuiltInAttribute($"{DefaultNames.SupportBlockIsBgraName}[0]",  AggregateType.Bool) },
-            { AttributeConsts.FragmentOutputIsBgraBase + 4,  new BuiltInAttribute($"{DefaultNames.SupportBlockIsBgraName}[1]",  AggregateType.Bool) },
-            { AttributeConsts.FragmentOutputIsBgraBase + 8,  new BuiltInAttribute($"{DefaultNames.SupportBlockIsBgraName}[2]",  AggregateType.Bool) },
-            { AttributeConsts.FragmentOutputIsBgraBase + 12, new BuiltInAttribute($"{DefaultNames.SupportBlockIsBgraName}[3]",  AggregateType.Bool) },
-            { AttributeConsts.FragmentOutputIsBgraBase + 16, new BuiltInAttribute($"{DefaultNames.SupportBlockIsBgraName}[4]",  AggregateType.Bool) },
-            { AttributeConsts.FragmentOutputIsBgraBase + 20, new BuiltInAttribute($"{DefaultNames.SupportBlockIsBgraName}[5]",  AggregateType.Bool) },
-            { AttributeConsts.FragmentOutputIsBgraBase + 24, new BuiltInAttribute($"{DefaultNames.SupportBlockIsBgraName}[6]",  AggregateType.Bool) },
-            { AttributeConsts.FragmentOutputIsBgraBase + 28, new BuiltInAttribute($"{DefaultNames.SupportBlockIsBgraName}[7]",  AggregateType.Bool) },
-
-            { AttributeConsts.SupportBlockViewInverseX,  new BuiltInAttribute($"{DefaultNames.SupportBlockViewportInverse}.x",  AggregateType.FP32) },
-            { AttributeConsts.SupportBlockViewInverseY,  new BuiltInAttribute($"{DefaultNames.SupportBlockViewportInverse}.y",  AggregateType.FP32) }
-        };
+        private static readonly string[] _stagePrefixes = new string[] { "cp", "vp", "tcp", "tep", "gp", "fp" };
 
         private Dictionary<AstOperand, string> _locals;
 
@@ -110,8 +35,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
             return operand.Type switch
             {
                 OperandType.Argument => GetArgumentName(operand.Value),
-                OperandType.Attribute => GetAttributeName(context, operand.Value, perPatch: false),
-                OperandType.AttributePerPatch => GetAttributeName(context, operand.Value, perPatch: true),
                 OperandType.Constant => NumberFormatter.FormatInt(operand.Value),
                 OperandType.ConstantBuffer => GetConstantBufferName(operand, context.Config),
                 OperandType.LocalVariable => _locals[operand],
@@ -155,177 +78,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
             return GetVec4Indexed(GetUbName(stage, slotExpr) + $"[{offsetExpr} >> 2]", offsetExpr + " & 3", indexElement);
         }
 
-        public static string GetOutAttributeName(CodeGenContext context, int value, bool perPatch)
-        {
-            return GetAttributeName(context, value, perPatch, isOutAttr: true);
-        }
-
-        public static string GetAttributeName(CodeGenContext context, int value, bool perPatch, bool isOutAttr = false, string indexExpr = "0")
-        {
-            ShaderConfig config = context.Config;
-
-            if ((value & AttributeConsts.LoadOutputMask) != 0)
-            {
-                isOutAttr = true;
-            }
-
-            value &= AttributeConsts.Mask & ~3;
-            char swzMask = GetSwizzleMask((value >> 2) & 3);
-
-            if (perPatch)
-            {
-                if (value >= AttributeConsts.UserAttributePerPatchBase && value < AttributeConsts.UserAttributePerPatchEnd)
-                {
-                    value -= AttributeConsts.UserAttributePerPatchBase;
-
-                    return $"{DefaultNames.PerPatchAttributePrefix}{(value >> 4)}.{swzMask}";
-                }
-                else if (value < AttributeConsts.UserAttributePerPatchBase)
-                {
-                    return value switch
-                    {
-                        AttributeConsts.TessLevelOuter0 => "gl_TessLevelOuter[0]",
-                        AttributeConsts.TessLevelOuter1 => "gl_TessLevelOuter[1]",
-                        AttributeConsts.TessLevelOuter2 => "gl_TessLevelOuter[2]",
-                        AttributeConsts.TessLevelOuter3 => "gl_TessLevelOuter[3]",
-                        AttributeConsts.TessLevelInner0 => "gl_TessLevelInner[0]",
-                        AttributeConsts.TessLevelInner1 => "gl_TessLevelInner[1]",
-                        _ => null
-                    };
-                }
-            }
-            else if (value >= AttributeConsts.UserAttributeBase && value < AttributeConsts.UserAttributeEnd)
-            {
-                int attrOffset = value;
-                value -= AttributeConsts.UserAttributeBase;
-
-                string prefix = isOutAttr
-                    ? DefaultNames.OAttributePrefix
-                    : DefaultNames.IAttributePrefix;
-
-                bool indexable = config.UsedFeatures.HasFlag(isOutAttr ? FeatureFlags.OaIndexing : FeatureFlags.IaIndexing);
-
-                if (indexable)
-                {
-                    string name = prefix;
-
-                    if (config.Stage == ShaderStage.Geometry && !isOutAttr)
-                    {
-                        name += $"[{indexExpr}]";
-                    }
-
-                    return name + $"[{(value >> 4)}]." + swzMask;
-                }
-                else if (config.TransformFeedbackEnabled &&
-                    ((config.LastInVertexPipeline && isOutAttr) ||
-                    (config.Stage == ShaderStage.Fragment && !isOutAttr)))
-                {
-                    int components = context.Info.GetTransformFeedbackOutputComponents(attrOffset);
-                    string name = components > 1 ? $"{prefix}{(value >> 4)}" : $"{prefix}{(value >> 4)}_{swzMask}";
-
-                    if (AttributeInfo.IsArrayAttributeGlsl(config.Stage, isOutAttr))
-                    {
-                        name += isOutAttr ? "[gl_InvocationID]" : $"[{indexExpr}]";
-                    }
-
-                    return components > 1 ? name + '.' + swzMask : name;
-                }
-                else
-                {
-                    string name = $"{prefix}{(value >> 4)}";
-
-                    if (AttributeInfo.IsArrayAttributeGlsl(config.Stage, isOutAttr))
-                    {
-                        name += isOutAttr ? "[gl_InvocationID]" : $"[{indexExpr}]";
-                    }
-
-                    return name + '.' + swzMask;
-                }
-            }
-            else
-            {
-                if (value >= AttributeConsts.FragmentOutputColorBase && value < AttributeConsts.FragmentOutputColorEnd)
-                {
-                    value -= AttributeConsts.FragmentOutputColorBase;
-
-                    return $"{DefaultNames.OAttributePrefix}{(value >> 4)}.{swzMask}";
-                }
-                else if (_builtInAttributes.TryGetValue(value, out BuiltInAttribute builtInAttr))
-                {
-                    string subgroupMask = value switch
-                    {
-                        AttributeConsts.EqMask => "Eq",
-                        AttributeConsts.GeMask => "Ge",
-                        AttributeConsts.GtMask => "Gt",
-                        AttributeConsts.LeMask => "Le",
-                        AttributeConsts.LtMask => "Lt",
-                        _ => null
-                    };
-
-                    if (subgroupMask != null)
-                    {
-                        return config.GpuAccessor.QueryHostSupportsShaderBallot()
-                            ? $"unpackUint2x32(gl_SubGroup{subgroupMask}MaskARB).x"
-                            : $"gl_Subgroup{subgroupMask}Mask.x";
-                    }
-                    else if (value == AttributeConsts.LaneId)
-                    {
-                        return config.GpuAccessor.QueryHostSupportsShaderBallot()
-                            ? "gl_SubGroupInvocationARB"
-                            : "gl_SubgroupInvocationID";
-                    }
-
-                    if (config.Stage == ShaderStage.Fragment)
-                    {
-                        // TODO: There must be a better way to handle this...
-                        switch (value)
-                        {
-                            case AttributeConsts.PositionX: return $"(gl_FragCoord.x / {DefaultNames.SupportBlockRenderScaleName}[0])";
-                            case AttributeConsts.PositionY: return $"(gl_FragCoord.y / {DefaultNames.SupportBlockRenderScaleName}[0])";
-                            case AttributeConsts.PositionZ: return "gl_FragCoord.z";
-                            case AttributeConsts.PositionW: return "gl_FragCoord.w";
-
-                            case AttributeConsts.FrontFacing:
-                                if (config.GpuAccessor.QueryHostHasFrontFacingBug())
-                                {
-                                    // This is required for Intel on Windows, gl_FrontFacing sometimes returns incorrect
-                                    // (flipped) values. Doing this seems to fix it.
-                                    return "(-floatBitsToInt(float(gl_FrontFacing)) < 0)";
-                                }
-                                break;
-                        }
-                    }
-
-                    string name = builtInAttr.Name;
-
-                    if (AttributeInfo.IsArrayAttributeGlsl(config.Stage, isOutAttr) && AttributeInfo.IsArrayBuiltIn(value))
-                    {
-                        name = isOutAttr ? $"gl_out[gl_InvocationID].{name}" : $"gl_in[{indexExpr}].{name}";
-                    }
-
-                    return name;
-                }
-            }
-
-            // TODO: Warn about unknown built-in attribute.
-
-            return isOutAttr ? "// bad_attr0x" + value.ToString("X") : "0.0";
-        }
-
-        public static string GetAttributeName(string attrExpr, ShaderConfig config, bool isOutAttr = false, string indexExpr = "0")
-        {
-            string name = isOutAttr
-                ? DefaultNames.OAttributePrefix
-                : DefaultNames.IAttributePrefix;
-
-            if (config.Stage == ShaderStage.Geometry && !isOutAttr)
-            {
-                name += $"[{indexExpr}]";
-            }
-
-            return $"{name}[{attrExpr} >> 2][{attrExpr} & 3]";
-        }
-
         public static string GetUbName(ShaderStage stage, int slot, bool cbIndexable)
         {
             if (cbIndexable)
@@ -387,12 +139,12 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
         {
             int index = (int)stage;
 
-            if ((uint)index >= StagePrefixes.Length)
+            if ((uint)index >= _stagePrefixes.Length)
             {
                 return "invalid";
             }
 
-            return StagePrefixes[index];
+            return _stagePrefixes[index];
         }
 
         private static char GetSwizzleMask(int value)
@@ -405,24 +157,54 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
             return $"{DefaultNames.ArgumentNamePrefix}{argIndex}";
         }
 
-        public static AggregateType GetNodeDestType(CodeGenContext context, IAstNode node, bool isAsgDest = false)
+        public static AggregateType GetNodeDestType(CodeGenContext context, IAstNode node)
         {
+            // TODO: Get rid of that function entirely and return the type from the operation generation
+            // functions directly, like SPIR-V does.
+
             if (node is AstOperation operation)
             {
-                if (operation.Inst == Instruction.LoadAttribute)
+                if (operation.Inst == Instruction.Load)
                 {
-                    // Load attribute basically just returns the attribute value.
-                    // Some built-in attributes may have different types, so we need
-                    // to return the type based on the attribute that is being read.
-                    if (operation.GetSource(0) is AstOperand operand && operand.Type == OperandType.Constant)
+                    switch (operation.StorageKind)
                     {
-                        if (_builtInAttributes.TryGetValue(operand.Value & ~3, out BuiltInAttribute builtInAttr))
-                        {
-                            return builtInAttr.Type;
-                        }
-                    }
+                        case StorageKind.Input:
+                        case StorageKind.InputPerPatch:
+                        case StorageKind.Output:
+                        case StorageKind.OutputPerPatch:
+                            if (!(operation.GetSource(0) is AstOperand varId) || varId.Type != OperandType.Constant)
+                            {
+                                throw new InvalidOperationException($"First input of {operation.Inst} with {operation.StorageKind} storage must be a constant operand.");
+                            }
+
+                            IoVariable ioVariable = (IoVariable)varId.Value;
+                            bool isOutput = operation.StorageKind == StorageKind.Output || operation.StorageKind == StorageKind.OutputPerPatch;
+                            bool isPerPatch = operation.StorageKind == StorageKind.InputPerPatch || operation.StorageKind == StorageKind.OutputPerPatch;
+                            int location = 0;
+                            int component = 0;
+
+                            if (context.Config.HasPerLocationInputOrOutput(ioVariable, isOutput))
+                            {
+                                if (!(operation.GetSource(1) is AstOperand vecIndex) || vecIndex.Type != OperandType.Constant)
+                                {
+                                    throw new InvalidOperationException($"Second input of {operation.Inst} with {operation.StorageKind} storage must be a constant operand.");
+                                }
+
+                                location = vecIndex.Value;
+
+                                if (operation.SourcesCount > 2 &&
+                                    operation.GetSource(2) is AstOperand elemIndex &&
+                                    elemIndex.Type == OperandType.Constant &&
+                                    context.Config.HasPerLocationInputOrOutputComponent(ioVariable, location, elemIndex.Value, isOutput))
+                                {
+                                    component = elemIndex.Value;
+                                }
+                            }
 
-                    return OperandInfo.GetVarType(OperandType.Attribute);
+                            (_, AggregateType varType) = IoMap.GetGlslVariable(context.Config, ioVariable, location, component, isOutput, isPerPatch);
+
+                            return varType & AggregateType.ElementTypeMask;
+                    }
                 }
                 else if (operation.Inst == Instruction.Call)
                 {
@@ -461,45 +243,12 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
                     return context.CurrentFunction.GetArgumentType(argIndex);
                 }
 
-                return GetOperandVarType(context, operand, isAsgDest);
+                return OperandInfo.GetVarType(operand);
             }
             else
             {
                 throw new ArgumentException($"Invalid node type \"{node?.GetType().Name ?? "null"}\".");
             }
         }
-
-        private static AggregateType GetOperandVarType(CodeGenContext context, AstOperand operand, bool isAsgDest = false)
-        {
-            if (operand.Type == OperandType.Attribute)
-            {
-                if (_builtInAttributes.TryGetValue(operand.Value & ~3, out BuiltInAttribute builtInAttr))
-                {
-                    return builtInAttr.Type;
-                }
-                else if (context.Config.Stage == ShaderStage.Vertex && !isAsgDest &&
-                    operand.Value >= AttributeConsts.UserAttributeBase &&
-                    operand.Value < AttributeConsts.UserAttributeEnd)
-                {
-                    int location = (operand.Value - AttributeConsts.UserAttributeBase) / 16;
-
-                    AttributeType type = context.Config.GpuAccessor.QueryAttributeType(location);
-
-                    return type.ToAggregateType();
-                }
-                else if (context.Config.Stage == ShaderStage.Fragment && isAsgDest &&
-                    operand.Value >= AttributeConsts.FragmentOutputColorBase &&
-                    operand.Value < AttributeConsts.FragmentOutputColorEnd)
-                {
-                    int location = (operand.Value - AttributeConsts.FragmentOutputColorBase) / 16;
-
-                    AttributeType type = context.Config.GpuAccessor.QueryFragmentOutputType(location);
-
-                    return type.ToAggregateType();
-                }
-            }
-
-            return OperandInfo.GetVarType(operand);
-        }
     }
 }
\ No newline at end of file
diff --git a/Ryujinx.Graphics.Shader/CodeGen/Spirv/CodeGenContext.cs b/Ryujinx.Graphics.Shader/CodeGen/Spirv/CodeGenContext.cs
index e693307d..ed292ef1 100644
--- a/Ryujinx.Graphics.Shader/CodeGen/Spirv/CodeGenContext.cs
+++ b/Ryujinx.Graphics.Shader/CodeGen/Spirv/CodeGenContext.cs
@@ -29,15 +29,13 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
         public Instruction StorageBuffersArray { get; set; }
         public Instruction LocalMemory { get; set; }
         public Instruction SharedMemory { get; set; }
-        public Instruction InputsArray { get; set; }
-        public Instruction OutputsArray { get; set; }
         public Dictionary<TextureMeta, SamplerType> SamplersTypes { get; } = new Dictionary<TextureMeta, SamplerType>();
         public Dictionary<TextureMeta, (Instruction, Instruction, Instruction)> Samplers { get; } = new Dictionary<TextureMeta, (Instruction, Instruction, Instruction)>();
         public Dictionary<TextureMeta, (Instruction, Instruction)> Images { get; } = new Dictionary<TextureMeta, (Instruction, Instruction)>();
-        public Dictionary<int, Instruction> Inputs { get; } = new Dictionary<int, Instruction>();
-        public Dictionary<int, Instruction> Outputs { get; } = new Dictionary<int, Instruction>();
-        public Dictionary<int, Instruction> InputsPerPatch { get; } = new Dictionary<int, Instruction>();
-        public Dictionary<int, Instruction> OutputsPerPatch { get; } = new Dictionary<int, Instruction>();
+        public Dictionary<IoDefinition, Instruction> Inputs { get; } = new Dictionary<IoDefinition, Instruction>();
+        public Dictionary<IoDefinition, Instruction> Outputs { get; } = new Dictionary<IoDefinition, Instruction>();
+        public Dictionary<IoDefinition, Instruction> InputsPerPatch { get; } = new Dictionary<IoDefinition, Instruction>();
+        public Dictionary<IoDefinition, Instruction> OutputsPerPatch { get; } = new Dictionary<IoDefinition, Instruction>();
 
         public Instruction CoordTemp { get; set; }
         private readonly Dictionary<AstOperand, Instruction> _locals = new Dictionary<AstOperand, Instruction>();
@@ -163,16 +161,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
             mainInterface.AddRange(InputsPerPatch.Values);
             mainInterface.AddRange(OutputsPerPatch.Values);
 
-            if (InputsArray != null)
-            {
-                mainInterface.Add(InputsArray);
-            }
-
-            if (OutputsArray != null)
-            {
-                mainInterface.Add(OutputsArray);
-            }
-
             return mainInterface.ToArray();
         }
 
@@ -228,8 +216,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
                 return operand.Type switch
                 {
                     IrOperandType.Argument => GetArgument(type, operand),
-                    IrOperandType.Attribute => GetAttribute(type, operand.Value & AttributeConsts.Mask, (operand.Value & AttributeConsts.LoadOutputMask) != 0),
-                    IrOperandType.AttributePerPatch => GetAttributePerPatch(type, operand.Value & AttributeConsts.Mask, (operand.Value & AttributeConsts.LoadOutputMask) != 0),
                     IrOperandType.Constant => GetConstant(type, operand),
                     IrOperandType.ConstantBuffer => GetConstantBuffer(type, operand),
                     IrOperandType.LocalVariable => GetLocal(type, operand),
@@ -275,239 +261,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
             };
         }
 
-        public Instruction GetAttributeElemPointer(int attr, bool isOutAttr, Instruction index, out AggregateType elemType)
-        {
-            var storageClass = isOutAttr ? StorageClass.Output : StorageClass.Input;
-            var attrInfo = AttributeInfo.From(Config, attr, isOutAttr);
-
-            int attrOffset = attrInfo.BaseValue;
-            AggregateType type = attrInfo.Type;
-
-            Instruction ioVariable, elemIndex;
-
-            Instruction invocationId = null;
-
-            if (Config.Stage == ShaderStage.TessellationControl && isOutAttr)
-            {
-                invocationId = Load(TypeS32(), Inputs[AttributeConsts.InvocationId]);
-            }
-
-            bool isUserAttr = attr >= AttributeConsts.UserAttributeBase && attr < AttributeConsts.UserAttributeEnd;
-
-            if (isUserAttr &&
-                ((!isOutAttr && Config.UsedFeatures.HasFlag(FeatureFlags.IaIndexing)) ||
-                (isOutAttr && Config.UsedFeatures.HasFlag(FeatureFlags.OaIndexing))))
-            {
-                elemType = AggregateType.FP32;
-                ioVariable = isOutAttr ? OutputsArray : InputsArray;
-                elemIndex = Constant(TypeU32(), attrInfo.GetInnermostIndex());
-                var vecIndex = Constant(TypeU32(), (attr - AttributeConsts.UserAttributeBase) >> 4);
-
-                bool isArray = AttributeInfo.IsArrayAttributeSpirv(Config.Stage, isOutAttr);
-
-                if (invocationId != null && isArray)
-                {
-                    return AccessChain(TypePointer(storageClass, GetType(elemType)), ioVariable, invocationId, index, vecIndex, elemIndex);
-                }
-                else if (invocationId != null)
-                {
-                    return AccessChain(TypePointer(storageClass, GetType(elemType)), ioVariable, invocationId, vecIndex, elemIndex);
-                }
-                else if (isArray)
-                {
-                    return AccessChain(TypePointer(storageClass, GetType(elemType)), ioVariable, index, vecIndex, elemIndex);
-                }
-                else
-                {
-                    return AccessChain(TypePointer(storageClass, GetType(elemType)), ioVariable, vecIndex, elemIndex);
-                }
-            }
-
-            bool isViewportInverse = attr == AttributeConsts.SupportBlockViewInverseX || attr == AttributeConsts.SupportBlockViewInverseY;
-
-            if (isViewportInverse)
-            {
-                elemType = AggregateType.FP32;
-                elemIndex = Constant(TypeU32(), (attr - AttributeConsts.SupportBlockViewInverseX) >> 2);
-                return AccessChain(TypePointer(StorageClass.Uniform, TypeFP32()), SupportBuffer, Constant(TypeU32(), 2), elemIndex);
-            }
-
-            elemType = attrInfo.Type & AggregateType.ElementTypeMask;
-
-            if (isUserAttr && Config.TransformFeedbackEnabled &&
-                ((isOutAttr && Config.LastInVertexPipeline) ||
-                (!isOutAttr && Config.Stage == ShaderStage.Fragment)))
-            {
-                attrOffset = attr;
-                type = elemType;
-
-                if (isOutAttr)
-                {
-                    int components = Info.GetTransformFeedbackOutputComponents(attr);
-
-                    if (components > 1)
-                    {
-                        attrOffset &= ~0xf;
-                        type = components switch
-                        {
-                            2 => AggregateType.Vector2 | AggregateType.FP32,
-                            3 => AggregateType.Vector3 | AggregateType.FP32,
-                            4 => AggregateType.Vector4 | AggregateType.FP32,
-                            _ => AggregateType.FP32
-                        };
-
-                        attrInfo = new AttributeInfo(attrOffset, (attr - attrOffset) / 4, components, type, false);
-                    }
-                }
-            }
-
-            ioVariable = isOutAttr ? Outputs[attrOffset] : Inputs[attrOffset];
-
-            bool isIndexed = AttributeInfo.IsArrayAttributeSpirv(Config.Stage, isOutAttr) && (!attrInfo.IsBuiltin || AttributeInfo.IsArrayBuiltIn(attr));
-
-            if ((type & (AggregateType.Array | AggregateType.ElementCountMask)) == 0)
-            {
-                if (invocationId != null)
-                {
-                    return isIndexed
-                        ? AccessChain(TypePointer(storageClass, GetType(elemType)), ioVariable, invocationId, index)
-                        : AccessChain(TypePointer(storageClass, GetType(elemType)), ioVariable, invocationId);
-                }
-                else
-                {
-                    return isIndexed ? AccessChain(TypePointer(storageClass, GetType(elemType)), ioVariable, index) : ioVariable;
-                }
-            }
-
-            elemIndex = Constant(TypeU32(), attrInfo.GetInnermostIndex());
-
-            if (invocationId != null && isIndexed)
-            {
-                return AccessChain(TypePointer(storageClass, GetType(elemType)), ioVariable, invocationId, index, elemIndex);
-            }
-            else if (invocationId != null)
-            {
-                return AccessChain(TypePointer(storageClass, GetType(elemType)), ioVariable, invocationId, elemIndex);
-            }
-            else if (isIndexed)
-            {
-                return AccessChain(TypePointer(storageClass, GetType(elemType)), ioVariable, index, elemIndex);
-            }
-            else
-            {
-                return AccessChain(TypePointer(storageClass, GetType(elemType)), ioVariable, elemIndex);
-            }
-        }
-
-        public Instruction GetAttributeElemPointer(Instruction attrIndex, bool isOutAttr, Instruction index, out AggregateType elemType)
-        {
-            var storageClass = isOutAttr ? StorageClass.Output : StorageClass.Input;
-
-            Instruction invocationId = null;
-
-            if (Config.Stage == ShaderStage.TessellationControl && isOutAttr)
-            {
-                invocationId = Load(TypeS32(), Inputs[AttributeConsts.InvocationId]);
-            }
-
-            elemType = AggregateType.FP32;
-            var ioVariable = isOutAttr ? OutputsArray : InputsArray;
-            var vecIndex = ShiftRightLogical(TypeS32(), attrIndex, Constant(TypeS32(), 2));
-            var elemIndex = BitwiseAnd(TypeS32(), attrIndex, Constant(TypeS32(), 3));
-
-            bool isArray = AttributeInfo.IsArrayAttributeSpirv(Config.Stage, isOutAttr);
-
-            if (invocationId != null && isArray)
-            {
-                return AccessChain(TypePointer(storageClass, GetType(elemType)), ioVariable, invocationId, index, vecIndex, elemIndex);
-            }
-            else if (invocationId != null)
-            {
-                return AccessChain(TypePointer(storageClass, GetType(elemType)), ioVariable, invocationId, vecIndex, elemIndex);
-            }
-            else if (isArray)
-            {
-                return AccessChain(TypePointer(storageClass, GetType(elemType)), ioVariable, index, vecIndex, elemIndex);
-            }
-            else
-            {
-                return AccessChain(TypePointer(storageClass, GetType(elemType)), ioVariable, vecIndex, elemIndex);
-            }
-        }
-
-        public Instruction GetAttribute(AggregateType type, int attr, bool isOutAttr, Instruction index = null)
-        {
-            if (!AttributeInfo.Validate(Config, attr, isOutAttr: false))
-            {
-                return GetConstant(type, new AstOperand(IrOperandType.Constant, 0));
-            }
-
-            var elemPointer = GetAttributeElemPointer(attr, isOutAttr, index, out var elemType);
-            var value = Load(GetType(elemType), elemPointer);
-
-            if (Config.Stage == ShaderStage.Fragment)
-            {
-                if (attr == AttributeConsts.PositionX || attr == AttributeConsts.PositionY)
-                {
-                    var pointerType = TypePointer(StorageClass.Uniform, TypeFP32());
-                    var fieldIndex = Constant(TypeU32(), 4);
-                    var scaleIndex = Constant(TypeU32(), 0);
-
-                    var scaleElemPointer = AccessChain(pointerType, SupportBuffer, fieldIndex, scaleIndex);
-                    var scale = Load(TypeFP32(), scaleElemPointer);
-
-                    value = FDiv(TypeFP32(), value, scale);
-                }
-                else if (attr == AttributeConsts.FrontFacing && Config.GpuAccessor.QueryHostHasFrontFacingBug())
-                {
-                    // Workaround for what appears to be a bug on Intel compiler.
-                    var valueFloat = Select(TypeFP32(), value, Constant(TypeFP32(), 1f), Constant(TypeFP32(), 0f));
-                    var valueAsInt = Bitcast(TypeS32(), valueFloat);
-                    var valueNegated = SNegate(TypeS32(), valueAsInt);
-
-                    value = SLessThan(TypeBool(), valueNegated, Constant(TypeS32(), 0));
-                }
-            }
-
-            return BitcastIfNeeded(type, elemType, value);
-        }
-
-        public Instruction GetAttributePerPatchElemPointer(int attr, bool isOutAttr, out AggregateType elemType)
-        {
-            var storageClass = isOutAttr ? StorageClass.Output : StorageClass.Input;
-            var attrInfo = AttributeInfo.FromPatch(Config, attr, isOutAttr);
-
-            int attrOffset = attrInfo.BaseValue;
-            Instruction ioVariable = isOutAttr ? OutputsPerPatch[attrOffset] : InputsPerPatch[attrOffset];
-
-            elemType = attrInfo.Type & AggregateType.ElementTypeMask;
-
-            if ((attrInfo.Type & (AggregateType.Array | AggregateType.ElementCountMask)) == 0)
-            {
-                return ioVariable;
-            }
-
-            var elemIndex = Constant(TypeU32(), attrInfo.GetInnermostIndex());
-            return AccessChain(TypePointer(storageClass, GetType(elemType)), ioVariable, elemIndex);
-        }
-
-        public Instruction GetAttributePerPatch(AggregateType type, int attr, bool isOutAttr)
-        {
-            if (!AttributeInfo.ValidatePerPatch(Config, attr, isOutAttr: false))
-            {
-                return GetConstant(type, new AstOperand(IrOperandType.Constant, 0));
-            }
-
-            var elemPointer = GetAttributePerPatchElemPointer(attr, isOutAttr, out var elemType);
-            return BitcastIfNeeded(type, elemType, Load(GetType(elemType), elemPointer));
-        }
-
-        public Instruction GetAttribute(AggregateType type, Instruction attr, bool isOutAttr, Instruction index = null)
-        {
-            var elemPointer = GetAttributeElemPointer(attr, isOutAttr, index, out var elemType);
-            return BitcastIfNeeded(type, elemType, Load(GetType(elemType), elemPointer));
-        }
-
         public Instruction GetConstant(AggregateType type, AstOperand operand)
         {
             return type switch
diff --git a/Ryujinx.Graphics.Shader/CodeGen/Spirv/Declarations.cs b/Ryujinx.Graphics.Shader/CodeGen/Spirv/Declarations.cs
index fdca5e89..821da477 100644
--- a/Ryujinx.Graphics.Shader/CodeGen/Spirv/Declarations.cs
+++ b/Ryujinx.Graphics.Shader/CodeGen/Spirv/Declarations.cs
@@ -1,21 +1,19 @@
 using Ryujinx.Common;
+using Ryujinx.Graphics.Shader.IntermediateRepresentation;
 using Ryujinx.Graphics.Shader.StructuredIr;
 using Ryujinx.Graphics.Shader.Translation;
 using Spv.Generator;
 using System;
 using System.Collections.Generic;
-using System.Diagnostics;
 using System.Linq;
 using System.Numerics;
 using static Spv.Specification;
+using SpvInstruction = Spv.Generator.Instruction;
 
 namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
 {
     static class Declarations
     {
-        // At least 16 attributes are guaranteed by the spec.
-        public const int MaxAttributes = 16;
-
         private static readonly string[] StagePrefixes = new string[] { "cp", "vp", "tcp", "tep", "gp", "fp" };
 
         public static void DeclareParameters(CodeGenContext context, StructuredFunction function)
@@ -59,7 +57,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
             for (int funcIndex = 0; funcIndex < functions.Count; funcIndex++)
             {
                 StructuredFunction function = functions[funcIndex];
-                Instruction[] locals = new Instruction[function.InArguments.Length];
+                SpvInstruction[] locals = new SpvInstruction[function.InArguments.Length];
 
                 for (int i = 0; i < function.InArguments.Length; i++)
                 {
@@ -105,10 +103,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
             DeclareStorageBuffers(context, context.Config.GetStorageBufferDescriptors());
             DeclareSamplers(context, context.Config.GetTextureDescriptors());
             DeclareImages(context, context.Config.GetImageDescriptors());
-            DeclareInputAttributes(context, info, perPatch: false);
-            DeclareOutputAttributes(context, info, perPatch: false);
-            DeclareInputAttributes(context, info, perPatch: true);
-            DeclareOutputAttributes(context, info, perPatch: true);
+            DeclareInputsAndOutputs(context, info);
         }
 
         private static void DeclareLocalMemory(CodeGenContext context, int size)
@@ -121,7 +116,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
             context.SharedMemory = DeclareMemory(context, StorageClass.Workgroup, size);
         }
 
-        private static Instruction DeclareMemory(CodeGenContext context, StorageClass storage, int size)
+        private static SpvInstruction DeclareMemory(CodeGenContext context, StorageClass storage, int size)
         {
             var arrayType = context.TypeArray(context.TypeU32(), context.Constant(context.TypeU32(), size));
             var pointerType = context.TypePointer(storage, arrayType);
@@ -395,164 +390,104 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
             };
         }
 
-        private static void DeclareInputAttributes(CodeGenContext context, StructuredProgramInfo info, bool perPatch)
+        private static void DeclareInputsAndOutputs(CodeGenContext context, StructuredProgramInfo info)
         {
-            bool iaIndexing = context.Config.UsedFeatures.HasFlag(FeatureFlags.IaIndexing);
-
-            if (iaIndexing && !perPatch)
+            foreach (var ioDefinition in info.IoDefinitions)
             {
-                var attrType = context.TypeVector(context.TypeFP32(), (LiteralInteger)4);
-                attrType = context.TypeArray(attrType, context.Constant(context.TypeU32(), (LiteralInteger)MaxAttributes));
-
-                if (context.Config.Stage == ShaderStage.Geometry)
-                {
-                    attrType = context.TypeArray(attrType, context.Constant(context.TypeU32(), (LiteralInteger)context.InputVertices));
-                }
-
-                var spvType = context.TypePointer(StorageClass.Input, attrType);
-                var spvVar = context.Variable(spvType, StorageClass.Input);
-
-                if (context.Config.PassthroughAttributes != 0 && context.Config.GpuAccessor.QueryHostSupportsGeometryShaderPassthrough())
-                {
-                    context.Decorate(spvVar, Decoration.PassthroughNV);
-                }
-
-                context.Decorate(spvVar, Decoration.Location, (LiteralInteger)0);
-
-                context.AddGlobalVariable(spvVar);
-                context.InputsArray = spvVar;
-            }
-
-            var inputs = perPatch ? info.InputsPerPatch : info.Inputs;
+                var ioVariable = ioDefinition.IoVariable;
 
-            foreach (int attr in inputs)
-            {
-                if (!AttributeInfo.Validate(context.Config, attr, isOutAttr: false, perPatch))
+                // Those are actually from constant buffer, rather than being actual inputs or outputs,
+                // so we must ignore them here as they are declared as part of the support buffer.
+                // TODO: Delete this after we represent this properly on the IR (as a constant buffer rather than "input").
+                if (ioVariable == IoVariable.FragmentOutputIsBgra ||
+                    ioVariable == IoVariable.SupportBlockRenderScale ||
+                    ioVariable == IoVariable.SupportBlockViewInverse)
                 {
                     continue;
                 }
 
-                bool isUserAttr = attr >= AttributeConsts.UserAttributeBase && attr < AttributeConsts.UserAttributeEnd;
-
-                if (iaIndexing && isUserAttr && !perPatch)
-                {
-                    continue;
-                }
+                bool isOutput = ioDefinition.StorageKind.IsOutput();
+                bool isPerPatch = ioDefinition.StorageKind.IsPerPatch();
 
                 PixelImap iq = PixelImap.Unused;
 
                 if (context.Config.Stage == ShaderStage.Fragment)
                 {
-                    if (attr >= AttributeConsts.UserAttributeBase && attr < AttributeConsts.UserAttributeEnd)
+                    if (ioVariable == IoVariable.UserDefined)
                     {
-                        iq = context.Config.ImapTypes[(attr - AttributeConsts.UserAttributeBase) / 16].GetFirstUsedType();
+                        iq = context.Config.ImapTypes[ioDefinition.Location].GetFirstUsedType();
                     }
                     else
                     {
-                        AttributeInfo attrInfo = AttributeInfo.From(context.Config, attr, isOutAttr: false);
-                        AggregateType elemType = attrInfo.Type & AggregateType.ElementTypeMask;
+                        (_, AggregateType varType) = IoMap.GetSpirvBuiltIn(ioVariable);
+                        AggregateType elemType = varType & AggregateType.ElementTypeMask;
 
-                        if (attrInfo.IsBuiltin && (elemType == AggregateType.S32 || elemType == AggregateType.U32))
+                        if (elemType == AggregateType.S32 || elemType == AggregateType.U32)
                         {
                             iq = PixelImap.Constant;
                         }
                     }
                 }
 
-                DeclareInputOrOutput(context, attr, perPatch, isOutAttr: false, iq);
+                DeclareInputOrOutput(context, ioDefinition, isOutput, isPerPatch, iq);
             }
         }
 
-        private static void DeclareOutputAttributes(CodeGenContext context, StructuredProgramInfo info, bool perPatch)
+        private static void DeclareInputOrOutput(CodeGenContext context, IoDefinition ioDefinition, bool isOutput, bool isPerPatch, PixelImap iq = PixelImap.Unused)
         {
-            bool oaIndexing = context.Config.UsedFeatures.HasFlag(FeatureFlags.OaIndexing);
-
-            if (oaIndexing && !perPatch)
-            {
-                var attrType = context.TypeVector(context.TypeFP32(), (LiteralInteger)4);
-                attrType = context.TypeArray(attrType, context.Constant(context.TypeU32(), (LiteralInteger)MaxAttributes));
-
-                if (context.Config.Stage == ShaderStage.TessellationControl)
-                {
-                    attrType = context.TypeArray(attrType, context.Constant(context.TypeU32(), context.Config.ThreadsPerInputPrimitive));
-                }
+            IoVariable ioVariable = ioDefinition.IoVariable;
+            var storageClass = isOutput ? StorageClass.Output : StorageClass.Input;
 
-                var spvType = context.TypePointer(StorageClass.Output, attrType);
-                var spvVar = context.Variable(spvType, StorageClass.Output);
+            bool isBuiltIn;
+            BuiltIn builtIn = default;
+            AggregateType varType;
 
-                context.Decorate(spvVar, Decoration.Location, (LiteralInteger)0);
-
-                context.AddGlobalVariable(spvVar);
-                context.OutputsArray = spvVar;
+            if (ioVariable == IoVariable.UserDefined)
+            {
+                varType = context.Config.GetUserDefinedType(ioDefinition.Location, isOutput);
+                isBuiltIn = false;
             }
-
-            var outputs = perPatch ? info.OutputsPerPatch : info.Outputs;
-
-            foreach (int attr in outputs)
+            else if (ioVariable == IoVariable.FragmentOutputColor)
             {
-                if (!AttributeInfo.Validate(context.Config, attr, isOutAttr: true, perPatch))
-                {
-                    continue;
-                }
-
-                bool isUserAttr = attr >= AttributeConsts.UserAttributeBase && attr < AttributeConsts.UserAttributeEnd;
+                varType = context.Config.GetFragmentOutputColorType(ioDefinition.Location);
+                isBuiltIn = false;
+            }
+            else
+            {
+                (builtIn, varType) = IoMap.GetSpirvBuiltIn(ioVariable);
+                isBuiltIn = true;
 
-                if (oaIndexing && isUserAttr && !perPatch)
+                if (varType == AggregateType.Invalid)
                 {
-                    continue;
+                    throw new InvalidOperationException($"Unknown variable {ioVariable}.");
                 }
-
-                DeclareOutputAttribute(context, attr, perPatch);
             }
 
-            if (context.Config.Stage == ShaderStage.Vertex)
-            {
-                DeclareOutputAttribute(context, AttributeConsts.PositionX, perPatch: false);
-            }
-        }
-
-        private static void DeclareOutputAttribute(CodeGenContext context, int attr, bool perPatch)
-        {
-            DeclareInputOrOutput(context, attr, perPatch, isOutAttr: true);
-        }
-
-        public static void DeclareInvocationId(CodeGenContext context)
-        {
-            DeclareInputOrOutput(context, AttributeConsts.LaneId, perPatch: false, isOutAttr: false);
-        }
+            bool hasComponent = context.Config.HasPerLocationInputOrOutputComponent(ioVariable, ioDefinition.Location, ioDefinition.Component, isOutput);
 
-        private static void DeclareInputOrOutput(CodeGenContext context, int attr, bool perPatch, bool isOutAttr, PixelImap iq = PixelImap.Unused)
-        {
-            bool isUserAttr = attr >= AttributeConsts.UserAttributeBase && attr < AttributeConsts.UserAttributeEnd;
-            if (isUserAttr && context.Config.TransformFeedbackEnabled && !perPatch &&
-                ((isOutAttr && context.Config.LastInVertexPipeline) ||
-                (!isOutAttr && context.Config.Stage == ShaderStage.Fragment)))
+            if (hasComponent)
             {
-                DeclareTransformFeedbackInputOrOutput(context, attr, isOutAttr, iq);
-                return;
+                varType &= AggregateType.ElementTypeMask;
             }
-
-            var dict = perPatch
-                ? (isOutAttr ? context.OutputsPerPatch : context.InputsPerPatch)
-                : (isOutAttr ? context.Outputs : context.Inputs);
-
-            var attrInfo = perPatch
-                ? AttributeInfo.FromPatch(context.Config, attr, isOutAttr)
-                : AttributeInfo.From(context.Config, attr, isOutAttr);
-
-            if (dict.ContainsKey(attrInfo.BaseValue))
+            else if (ioVariable == IoVariable.UserDefined && context.Config.HasTransformFeedbackOutputs(isOutput))
             {
-                return;
+                varType &= AggregateType.ElementTypeMask;
+                varType |= context.Config.GetTransformFeedbackOutputComponents(ioDefinition.Location, ioDefinition.Component) switch
+                {
+                    2 => AggregateType.Vector2,
+                    3 => AggregateType.Vector3,
+                    4 => AggregateType.Vector4,
+                    _ => AggregateType.Invalid
+                };
             }
 
-            var storageClass = isOutAttr ? StorageClass.Output : StorageClass.Input;
-            var attrType = context.GetType(attrInfo.Type, attrInfo.Length);
+            var spvType = context.GetType(varType, IoMap.GetSpirvBuiltInArrayLength(ioVariable));
             bool builtInPassthrough = false;
 
-            if (AttributeInfo.IsArrayAttributeSpirv(context.Config.Stage, isOutAttr) && !perPatch && (!attrInfo.IsBuiltin || AttributeInfo.IsArrayBuiltIn(attr)))
+            if (!isPerPatch && IoMap.IsPerVertex(ioVariable, context.Config.Stage, isOutput))
             {
                 int arraySize = context.Config.Stage == ShaderStage.Geometry ? context.InputVertices : 32;
-                attrType = context.TypeArray(attrType, context.Constant(context.TypeU32(), (LiteralInteger)arraySize));
+                spvType = context.TypeArray(spvType, context.Constant(context.TypeU32(), (LiteralInteger)arraySize));
 
                 if (context.Config.GpPassthrough && context.Config.GpuAccessor.QueryHostSupportsGeometryShaderPassthrough())
                 {
@@ -560,69 +495,64 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
                 }
             }
 
-            if (context.Config.Stage == ShaderStage.TessellationControl && isOutAttr && !perPatch)
+            if (context.Config.Stage == ShaderStage.TessellationControl && isOutput && !isPerPatch)
             {
-                attrType = context.TypeArray(attrType, context.Constant(context.TypeU32(), context.Config.ThreadsPerInputPrimitive));
+                spvType = context.TypeArray(spvType, context.Constant(context.TypeU32(), context.Config.ThreadsPerInputPrimitive));
             }
 
-            var spvType = context.TypePointer(storageClass, attrType);
-            var spvVar = context.Variable(spvType, storageClass);
+            var spvPointerType = context.TypePointer(storageClass, spvType);
+            var spvVar = context.Variable(spvPointerType, storageClass);
 
             if (builtInPassthrough)
             {
                 context.Decorate(spvVar, Decoration.PassthroughNV);
             }
 
-            if (attrInfo.IsBuiltin)
+            if (isBuiltIn)
             {
-                if (perPatch)
+                if (isPerPatch)
                 {
                     context.Decorate(spvVar, Decoration.Patch);
                 }
 
-                if (context.Config.GpuAccessor.QueryHostReducedPrecision() && attr == AttributeConsts.PositionX && context.Config.Stage != ShaderStage.Fragment)
+                if (context.Config.GpuAccessor.QueryHostReducedPrecision() && ioVariable == IoVariable.Position)
                 {
                     context.Decorate(spvVar, Decoration.Invariant);
                 }
 
-                context.Decorate(spvVar, Decoration.BuiltIn, (LiteralInteger)GetBuiltIn(context, attrInfo.BaseValue));
-
-                if (context.Config.TransformFeedbackEnabled && context.Config.LastInVertexPipeline && isOutAttr)
-                {
-                    var tfOutput = context.Info.GetTransformFeedbackOutput(attrInfo.BaseValue);
-                    if (tfOutput.Valid)
-                    {
-                        context.Decorate(spvVar, Decoration.XfbBuffer, (LiteralInteger)tfOutput.Buffer);
-                        context.Decorate(spvVar, Decoration.XfbStride, (LiteralInteger)tfOutput.Stride);
-                        context.Decorate(spvVar, Decoration.Offset, (LiteralInteger)tfOutput.Offset);
-                    }
-                }
+                context.Decorate(spvVar, Decoration.BuiltIn, (LiteralInteger)builtIn);
             }
-            else if (perPatch)
+            else if (isPerPatch)
             {
                 context.Decorate(spvVar, Decoration.Patch);
 
-                int location = context.Config.GetPerPatchAttributeLocation((attr - AttributeConsts.UserAttributePerPatchBase) / 16);
+                if (ioVariable == IoVariable.UserDefined)
+                {
+                    int location = context.Config.GetPerPatchAttributeLocation(ioDefinition.Location);
 
-                context.Decorate(spvVar, Decoration.Location, (LiteralInteger)location);
+                    context.Decorate(spvVar, Decoration.Location, (LiteralInteger)location);
+                }
             }
-            else if (isUserAttr)
+            else if (ioVariable == IoVariable.UserDefined)
             {
-                int location = (attr - AttributeConsts.UserAttributeBase) / 16;
+                context.Decorate(spvVar, Decoration.Location, (LiteralInteger)ioDefinition.Location);
 
-                context.Decorate(spvVar, Decoration.Location, (LiteralInteger)location);
+                if (hasComponent)
+                {
+                    context.Decorate(spvVar, Decoration.Component, (LiteralInteger)ioDefinition.Component);
+                }
 
-                if (!isOutAttr &&
-                    !perPatch &&
-                    (context.Config.PassthroughAttributes & (1 << location)) != 0 &&
+                if (!isOutput &&
+                    !isPerPatch &&
+                    (context.Config.PassthroughAttributes & (1 << ioDefinition.Location)) != 0 &&
                     context.Config.GpuAccessor.QueryHostSupportsGeometryShaderPassthrough())
                 {
                     context.Decorate(spvVar, Decoration.PassthroughNV);
                 }
             }
-            else if (attr >= AttributeConsts.FragmentOutputColorBase && attr < AttributeConsts.FragmentOutputColorEnd)
+            else if (ioVariable == IoVariable.FragmentOutputColor)
             {
-                int location = (attr - AttributeConsts.FragmentOutputColorBase) / 16;
+                int location = ioDefinition.Location;
 
                 if (context.Config.Stage == ShaderStage.Fragment && context.Config.GpuAccessor.QueryDualSourceBlendEnable())
                 {
@@ -646,7 +576,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
                 }
             }
 
-            if (!isOutAttr)
+            if (!isOutput)
             {
                 switch (iq)
                 {
@@ -658,143 +588,23 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
                         break;
                 }
             }
-
-            context.AddGlobalVariable(spvVar);
-            dict.Add(attrInfo.BaseValue, spvVar);
-        }
-
-        private static void DeclareTransformFeedbackInputOrOutput(CodeGenContext context, int attr, bool isOutAttr, PixelImap iq = PixelImap.Unused)
-        {
-            var dict = isOutAttr ? context.Outputs : context.Inputs;
-            var attrInfo = AttributeInfo.From(context.Config, attr, isOutAttr);
-
-            bool hasComponent = true;
-            int component = (attr >> 2) & 3;
-            int components = 1;
-            var type = attrInfo.Type & AggregateType.ElementTypeMask;
-
-            if (isOutAttr)
+            else if (context.Config.TryGetTransformFeedbackOutput(
+                ioVariable,
+                ioDefinition.Location,
+                ioDefinition.Component,
+                out var transformFeedbackOutput))
             {
-                components = context.Info.GetTransformFeedbackOutputComponents(attr);
-
-                if (components > 1)
-                {
-                    attr &= ~0xf;
-                    type = components switch
-                    {
-                        2 => AggregateType.Vector2 | AggregateType.FP32,
-                        3 => AggregateType.Vector3 | AggregateType.FP32,
-                        4 => AggregateType.Vector4 | AggregateType.FP32,
-                        _ => AggregateType.FP32
-                    };
-
-                    hasComponent = false;
-                }
-            }
-
-            if (dict.ContainsKey(attr))
-            {
-                return;
-            }
-
-            var storageClass = isOutAttr ? StorageClass.Output : StorageClass.Input;
-            var attrType = context.GetType(type, components);
-
-            if (AttributeInfo.IsArrayAttributeSpirv(context.Config.Stage, isOutAttr) && (!attrInfo.IsBuiltin || AttributeInfo.IsArrayBuiltIn(attr)))
-            {
-                int arraySize = context.Config.Stage == ShaderStage.Geometry ? context.InputVertices : 32;
-                attrType = context.TypeArray(attrType, context.Constant(context.TypeU32(), (LiteralInteger)arraySize));
-            }
-
-            if (context.Config.Stage == ShaderStage.TessellationControl && isOutAttr)
-            {
-                attrType = context.TypeArray(attrType, context.Constant(context.TypeU32(), context.Config.ThreadsPerInputPrimitive));
-            }
-
-            var spvType = context.TypePointer(storageClass, attrType);
-            var spvVar = context.Variable(spvType, storageClass);
-
-            Debug.Assert(attr >= AttributeConsts.UserAttributeBase && attr < AttributeConsts.UserAttributeEnd);
-            int location = (attr - AttributeConsts.UserAttributeBase) / 16;
-
-            context.Decorate(spvVar, Decoration.Location, (LiteralInteger)location);
-
-            if (hasComponent)
-            {
-                context.Decorate(spvVar, Decoration.Component, (LiteralInteger)component);
-            }
-
-            if (isOutAttr)
-            {
-                var tfOutput = context.Info.GetTransformFeedbackOutput(attr);
-                if (tfOutput.Valid)
-                {
-                    context.Decorate(spvVar, Decoration.XfbBuffer, (LiteralInteger)tfOutput.Buffer);
-                    context.Decorate(spvVar, Decoration.XfbStride, (LiteralInteger)tfOutput.Stride);
-                    context.Decorate(spvVar, Decoration.Offset, (LiteralInteger)tfOutput.Offset);
-                }
-            }
-            else
-            {
-                if ((context.Config.PassthroughAttributes & (1 << location)) != 0 &&
-                    context.Config.GpuAccessor.QueryHostSupportsGeometryShaderPassthrough())
-                {
-                    context.Decorate(spvVar, Decoration.PassthroughNV);
-                }
-
-                switch (iq)
-                {
-                    case PixelImap.Constant:
-                        context.Decorate(spvVar, Decoration.Flat);
-                        break;
-                    case PixelImap.ScreenLinear:
-                        context.Decorate(spvVar, Decoration.NoPerspective);
-                        break;
-                }
+                context.Decorate(spvVar, Decoration.XfbBuffer, (LiteralInteger)transformFeedbackOutput.Buffer);
+                context.Decorate(spvVar, Decoration.XfbStride, (LiteralInteger)transformFeedbackOutput.Stride);
+                context.Decorate(spvVar, Decoration.Offset, (LiteralInteger)transformFeedbackOutput.Offset);
             }
 
             context.AddGlobalVariable(spvVar);
-            dict.Add(attr, spvVar);
-        }
 
-        private static BuiltIn GetBuiltIn(CodeGenContext context, int attr)
-        {
-            return attr switch
-            {
-                AttributeConsts.TessLevelOuter0 => BuiltIn.TessLevelOuter,
-                AttributeConsts.TessLevelInner0 => BuiltIn.TessLevelInner,
-                AttributeConsts.Layer => BuiltIn.Layer,
-                AttributeConsts.ViewportIndex => BuiltIn.ViewportIndex,
-                AttributeConsts.PointSize => BuiltIn.PointSize,
-                AttributeConsts.PositionX => context.Config.Stage == ShaderStage.Fragment ? BuiltIn.FragCoord : BuiltIn.Position,
-                AttributeConsts.ClipDistance0 => BuiltIn.ClipDistance,
-                AttributeConsts.PointCoordX => BuiltIn.PointCoord,
-                AttributeConsts.TessCoordX => BuiltIn.TessCoord,
-                AttributeConsts.InstanceId => BuiltIn.InstanceId,
-                AttributeConsts.VertexId => BuiltIn.VertexId,
-                AttributeConsts.BaseInstance => BuiltIn.BaseInstance,
-                AttributeConsts.BaseVertex => BuiltIn.BaseVertex,
-                AttributeConsts.InstanceIndex => BuiltIn.InstanceIndex,
-                AttributeConsts.VertexIndex => BuiltIn.VertexIndex,
-                AttributeConsts.DrawIndex => BuiltIn.DrawIndex,
-                AttributeConsts.FrontFacing => BuiltIn.FrontFacing,
-                AttributeConsts.FragmentOutputDepth => BuiltIn.FragDepth,
-                AttributeConsts.ThreadKill => BuiltIn.HelperInvocation,
-                AttributeConsts.ThreadIdX => BuiltIn.LocalInvocationId,
-                AttributeConsts.CtaIdX => BuiltIn.WorkgroupId,
-                AttributeConsts.LaneId => BuiltIn.SubgroupLocalInvocationId,
-                AttributeConsts.InvocationId => BuiltIn.InvocationId,
-                AttributeConsts.PrimitiveId => BuiltIn.PrimitiveId,
-                AttributeConsts.PatchVerticesIn => BuiltIn.PatchVertices,
-                AttributeConsts.EqMask => BuiltIn.SubgroupEqMask,
-                AttributeConsts.GeMask => BuiltIn.SubgroupGeMask,
-                AttributeConsts.GtMask => BuiltIn.SubgroupGtMask,
-                AttributeConsts.LeMask => BuiltIn.SubgroupLeMask,
-                AttributeConsts.LtMask => BuiltIn.SubgroupLtMask,
-                AttributeConsts.SupportBlockViewInverseX => BuiltIn.Position,
-                AttributeConsts.SupportBlockViewInverseY => BuiltIn.Position,
-                _ => throw new ArgumentException($"Invalid attribute number 0x{attr:X}.")
-            };
+            var dict = isPerPatch
+                ? (isOutput ? context.OutputsPerPatch : context.InputsPerPatch)
+                : (isOutput ? context.Outputs : context.Inputs);
+            dict.Add(ioDefinition, spvVar);
         }
 
         private static string GetStagePrefix(ShaderStage stage)
diff --git a/Ryujinx.Graphics.Shader/CodeGen/Spirv/EnumConversion.cs b/Ryujinx.Graphics.Shader/CodeGen/Spirv/EnumConversion.cs
index aa3d046a..72541774 100644
--- a/Ryujinx.Graphics.Shader/CodeGen/Spirv/EnumConversion.cs
+++ b/Ryujinx.Graphics.Shader/CodeGen/Spirv/EnumConversion.cs
@@ -1,5 +1,4 @@
-using Ryujinx.Graphics.Shader.Translation;
-using System;
+using System;
 using static Spv.Specification;
 
 namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
diff --git a/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs b/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs
index b3db1905..b6ffdb7a 100644
--- a/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs
+++ b/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs
@@ -97,7 +97,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
             Add(Instruction.ImageLoad,                GenerateImageLoad);
             Add(Instruction.ImageStore,               GenerateImageStore);
             Add(Instruction.IsNan,                    GenerateIsNan);
-            Add(Instruction.LoadAttribute,            GenerateLoadAttribute);
+            Add(Instruction.Load,                     GenerateLoad);
             Add(Instruction.LoadConstant,             GenerateLoadConstant);
             Add(Instruction.LoadLocal,                GenerateLoadLocal);
             Add(Instruction.LoadShared,               GenerateLoadShared);
@@ -133,7 +133,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
             Add(Instruction.ShuffleXor,               GenerateShuffleXor);
             Add(Instruction.Sine,                     GenerateSine);
             Add(Instruction.SquareRoot,               GenerateSquareRoot);
-            Add(Instruction.StoreAttribute,           GenerateStoreAttribute);
+            Add(Instruction.Store,                    GenerateStore);
             Add(Instruction.StoreLocal,               GenerateStoreLocal);
             Add(Instruction.StoreShared,              GenerateStoreShared);
             Add(Instruction.StoreShared16,            GenerateStoreShared16);
@@ -862,31 +862,9 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
             return new OperationResult(AggregateType.Bool, result);
         }
 
-        private static OperationResult GenerateLoadAttribute(CodeGenContext context, AstOperation operation)
+        private static OperationResult GenerateLoad(CodeGenContext context, AstOperation operation)
         {
-            var src1 = operation.GetSource(0);
-            var src2 = operation.GetSource(1);
-            var src3 = operation.GetSource(2);
-
-            if (!(src1 is AstOperand baseAttr) || baseAttr.Type != OperandType.Constant)
-            {
-                throw new InvalidOperationException($"First input of {nameof(Instruction.LoadAttribute)} must be a constant operand.");
-            }
-
-            var index = context.Get(AggregateType.S32, src3);
-            var resultType = AggregateType.FP32;
-
-            if (src2 is AstOperand operand && operand.Type == OperandType.Constant)
-            {
-                int attrOffset = (baseAttr.Value & AttributeConsts.Mask) + (operand.Value << 2);
-                bool isOutAttr = (baseAttr.Value & AttributeConsts.LoadOutputMask) != 0;
-                return new OperationResult(resultType, context.GetAttribute(resultType, attrOffset, isOutAttr, index));
-            }
-            else
-            {
-                var attr = context.Get(AggregateType.S32, src2);
-                return new OperationResult(resultType, context.GetAttribute(resultType, attr, isOutAttr: false, index));
-            }
+            return GenerateLoadOrStore(context, operation, isStore: false);
         }
 
         private static OperationResult GenerateLoadConstant(CodeGenContext context, AstOperation operation)
@@ -1224,7 +1202,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
             var clampNotSegMask = context.BitwiseAnd(context.TypeU32(), clamp, notSegMask);
             var indexNotSegMask = context.BitwiseAnd(context.TypeU32(), index, notSegMask);
 
-            var threadId = context.GetAttribute(AggregateType.U32, AttributeConsts.LaneId, false);
+            var threadId = GetScalarInput(context, IoVariable.SubgroupLaneId);
 
             var minThreadId = context.BitwiseAnd(context.TypeU32(), threadId, segMask);
             var maxThreadId = context.BitwiseOr(context.TypeU32(), minThreadId, clampNotSegMask);
@@ -1254,7 +1232,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
             var notSegMask = context.Not(context.TypeU32(), segMask);
             var clampNotSegMask = context.BitwiseAnd(context.TypeU32(), clamp, notSegMask);
 
-            var threadId = context.GetAttribute(AggregateType.U32, AttributeConsts.LaneId, false);
+            var threadId = GetScalarInput(context, IoVariable.SubgroupLaneId);
 
             var minThreadId = context.BitwiseAnd(context.TypeU32(), threadId, segMask);
             var maxThreadId = context.BitwiseOr(context.TypeU32(), minThreadId, clampNotSegMask);
@@ -1281,7 +1259,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
 
             var segMask = context.BitwiseAnd(context.TypeU32(), context.ShiftRightLogical(context.TypeU32(), mask, const8), const31);
 
-            var threadId = context.GetAttribute(AggregateType.U32, AttributeConsts.LaneId, false);
+            var threadId = GetScalarInput(context, IoVariable.SubgroupLaneId);
 
             var minThreadId = context.BitwiseAnd(context.TypeU32(), threadId, segMask);
             var srcThreadId = context.ISub(context.TypeU32(), threadId, index);
@@ -1310,7 +1288,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
             var notSegMask = context.Not(context.TypeU32(), segMask);
             var clampNotSegMask = context.BitwiseAnd(context.TypeU32(), clamp, notSegMask);
 
-            var threadId = context.GetAttribute(AggregateType.U32, AttributeConsts.LaneId, false);
+            var threadId = GetScalarInput(context, IoVariable.SubgroupLaneId);
 
             var minThreadId = context.BitwiseAnd(context.TypeU32(), threadId, segMask);
             var maxThreadId = context.BitwiseOr(context.TypeU32(), minThreadId, clampNotSegMask);
@@ -1336,35 +1314,9 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
             return GenerateUnary(context, operation, context.Delegates.GlslSqrt, null);
         }
 
-        private static OperationResult GenerateStoreAttribute(CodeGenContext context, AstOperation operation)
+        private static OperationResult GenerateStore(CodeGenContext context, AstOperation operation)
         {
-            var src1 = operation.GetSource(0);
-            var src2 = operation.GetSource(1);
-            var src3 = operation.GetSource(2);
-
-            if (!(src1 is AstOperand baseAttr) || baseAttr.Type != OperandType.Constant)
-            {
-                throw new InvalidOperationException($"First input of {nameof(Instruction.StoreAttribute)} must be a constant operand.");
-            }
-
-            SpvInstruction elemPointer;
-            AggregateType elemType;
-
-            if (src2 is AstOperand operand && operand.Type == OperandType.Constant)
-            {
-                int attrOffset = (baseAttr.Value & AttributeConsts.Mask) + (operand.Value << 2);
-                elemPointer = context.GetAttributeElemPointer(attrOffset, isOutAttr: true, index: null, out elemType);
-            }
-            else
-            {
-                var attr = context.Get(AggregateType.S32, src2);
-                elemPointer = context.GetAttributeElemPointer(attr, isOutAttr: true, index: null, out elemType);
-            }
-
-            var value = context.Get(elemType, src3);
-            context.Store(elemPointer, value);
-
-            return OperationResult.Invalid;
+            return GenerateLoadOrStore(context, operation, isStore: true);
         }
 
         private static OperationResult GenerateStoreLocal(CodeGenContext context, AstOperation operation)
@@ -1448,7 +1400,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
 
             var three = context.Constant(context.TypeU32(), 3);
 
-            var threadId = context.GetAttribute(AggregateType.U32, AttributeConsts.LaneId, false);
+            var threadId = GetScalarInput(context, IoVariable.SubgroupLaneId);
             var shift = context.BitwiseAnd(context.TypeU32(), threadId, three);
             shift = context.ShiftLeftLogical(context.TypeU32(), shift, context.Constant(context.TypeU32(), 1));
             var lutIdx = context.ShiftRightLogical(context.TypeU32(), mask, shift);
@@ -1982,20 +1934,19 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
             var value = context.GetU32(operation.GetSource(2));
 
             SpvInstruction elemPointer;
-            Instruction mr = operation.Inst & Instruction.MrMask;
 
-            if (mr == Instruction.MrStorage)
+            if (operation.StorageKind == StorageKind.StorageBuffer)
             {
                 elemPointer = GetStorageElemPointer(context, operation);
             }
-            else if (mr == Instruction.MrShared)
+            else if (operation.StorageKind == StorageKind.SharedMemory)
             {
                 var offset = context.GetU32(operation.GetSource(0));
                 elemPointer = context.AccessChain(context.TypePointer(StorageClass.Workgroup, context.TypeU32()), context.SharedMemory, offset);
             }
             else
             {
-                throw new InvalidOperationException($"Invalid storage class \"{mr}\".");
+                throw new InvalidOperationException($"Invalid storage kind \"{operation.StorageKind}\".");
             }
 
             var one = context.Constant(context.TypeU32(), 1);
@@ -2010,20 +1961,19 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
             var value1 = context.GetU32(operation.GetSource(3));
 
             SpvInstruction elemPointer;
-            Instruction mr = operation.Inst & Instruction.MrMask;
 
-            if (mr == Instruction.MrStorage)
+            if (operation.StorageKind == StorageKind.StorageBuffer)
             {
                 elemPointer = GetStorageElemPointer(context, operation);
             }
-            else if (mr == Instruction.MrShared)
+            else if (operation.StorageKind == StorageKind.SharedMemory)
             {
                 var offset = context.GetU32(operation.GetSource(0));
                 elemPointer = context.AccessChain(context.TypePointer(StorageClass.Workgroup, context.TypeU32()), context.SharedMemory, offset);
             }
             else
             {
-                throw new InvalidOperationException($"Invalid storage class \"{mr}\".");
+                throw new InvalidOperationException($"Invalid storage kind \"{operation.StorageKind}\".");
             }
 
             var one = context.Constant(context.TypeU32(), 1);
@@ -2032,6 +1982,163 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
             return new OperationResult(AggregateType.U32, context.AtomicCompareExchange(context.TypeU32(), elemPointer, one, zero, zero, value1, value0));
         }
 
+        private static OperationResult GenerateLoadOrStore(CodeGenContext context, AstOperation operation, bool isStore)
+        {
+            StorageKind storageKind = operation.StorageKind;
+
+            SpvInstruction pointer;
+            AggregateType varType;
+            int srcIndex = 0;
+
+            switch (storageKind)
+            {
+                case StorageKind.Input:
+                case StorageKind.InputPerPatch:
+                case StorageKind.Output:
+                case StorageKind.OutputPerPatch:
+                    if (!(operation.GetSource(srcIndex++) is AstOperand varId) || varId.Type != OperandType.Constant)
+                    {
+                        throw new InvalidOperationException($"First input of {operation.Inst} with {storageKind} storage must be a constant operand.");
+                    }
+
+                    IoVariable ioVariable = (IoVariable)varId.Value;
+                    bool isOutput = storageKind.IsOutput();
+                    bool isPerPatch = storageKind.IsPerPatch();
+                    int location = 0;
+                    int component = 0;
+
+                    if (context.Config.HasPerLocationInputOrOutput(ioVariable, isOutput))
+                    {
+                        if (!(operation.GetSource(srcIndex++) is AstOperand vecIndex) || vecIndex.Type != OperandType.Constant)
+                        {
+                            throw new InvalidOperationException($"Second input of {operation.Inst} with {storageKind} storage must be a constant operand.");
+                        }
+
+                        location = vecIndex.Value;
+
+                        if (operation.SourcesCount > srcIndex &&
+                            operation.GetSource(srcIndex) is AstOperand elemIndex &&
+                            elemIndex.Type == OperandType.Constant &&
+                            context.Config.HasPerLocationInputOrOutputComponent(ioVariable, location, elemIndex.Value, isOutput))
+                        {
+                            component = elemIndex.Value;
+                            srcIndex++;
+                        }
+                    }
+
+                    if (ioVariable == IoVariable.UserDefined)
+                    {
+                        varType = context.Config.GetUserDefinedType(location, isOutput);
+                    }
+                    else if (ioVariable == IoVariable.FragmentOutputColor)
+                    {
+                        varType = context.Config.GetFragmentOutputColorType(location);
+                    }
+                    else if (ioVariable == IoVariable.FragmentOutputIsBgra)
+                    {
+                        var pointerType = context.TypePointer(StorageClass.Uniform, context.TypeU32());
+                        var elemIndex = context.Get(AggregateType.S32, operation.GetSource(srcIndex++));
+                        pointer = context.AccessChain(pointerType, context.SupportBuffer, context.Constant(context.TypeU32(), 1), elemIndex);
+                        varType = AggregateType.U32;
+
+                        break;
+                    }
+                    else if (ioVariable == IoVariable.SupportBlockRenderScale)
+                    {
+                        var pointerType = context.TypePointer(StorageClass.Uniform, context.TypeFP32());
+                        var elemIndex = context.Get(AggregateType.S32, operation.GetSource(srcIndex++));
+                        pointer = context.AccessChain(pointerType, context.SupportBuffer, context.Constant(context.TypeU32(), 4), elemIndex);
+                        varType = AggregateType.FP32;
+
+                        break;
+                    }
+                    else if (ioVariable == IoVariable.SupportBlockViewInverse)
+                    {
+                        var pointerType = context.TypePointer(StorageClass.Uniform, context.TypeFP32());
+                        var elemIndex = context.Get(AggregateType.S32, operation.GetSource(srcIndex++));
+                        pointer = context.AccessChain(pointerType, context.SupportBuffer, context.Constant(context.TypeU32(), 2), elemIndex);
+                        varType = AggregateType.FP32;
+
+                        break;
+                    }
+                    else
+                    {
+                        (_, varType) = IoMap.GetSpirvBuiltIn(ioVariable);
+                    }
+
+                    varType &= AggregateType.ElementTypeMask;
+
+                    int inputsCount = (isStore ? operation.SourcesCount - 1 : operation.SourcesCount) - srcIndex;
+                    var storageClass = isOutput ? StorageClass.Output : StorageClass.Input;
+
+                    var ioDefinition = new IoDefinition(storageKind, ioVariable, location, component);
+                    var dict = isPerPatch
+                        ? (isOutput ? context.OutputsPerPatch : context.InputsPerPatch)
+                        : (isOutput ? context.Outputs : context.Inputs);
+
+                    SpvInstruction baseObj = dict[ioDefinition];
+                    SpvInstruction e0, e1, e2;
+
+                    switch (inputsCount)
+                    {
+                        case 0:
+                            pointer = baseObj;
+                            break;
+                        case 1:
+                            e0 = context.Get(AggregateType.S32, operation.GetSource(srcIndex++));
+                            pointer = context.AccessChain(context.TypePointer(storageClass, context.GetType(varType)), baseObj, e0);
+                            break;
+                        case 2:
+                            e0 = context.Get(AggregateType.S32, operation.GetSource(srcIndex++));
+                            e1 = context.Get(AggregateType.S32, operation.GetSource(srcIndex++));
+                            pointer = context.AccessChain(context.TypePointer(storageClass, context.GetType(varType)), baseObj, e0, e1);
+                            break;
+                        case 3:
+                            e0 = context.Get(AggregateType.S32, operation.GetSource(srcIndex++));
+                            e1 = context.Get(AggregateType.S32, operation.GetSource(srcIndex++));
+                            e2 = context.Get(AggregateType.S32, operation.GetSource(srcIndex++));
+                            pointer = context.AccessChain(context.TypePointer(storageClass, context.GetType(varType)), baseObj, e0, e1, e2);
+                            break;
+                        default:
+                            var indexes = new SpvInstruction[inputsCount];
+                            int index = 0;
+
+                            for (; index < inputsCount; srcIndex++, index++)
+                            {
+                                indexes[index] = context.Get(AggregateType.S32, operation.GetSource(srcIndex));
+                            }
+
+                            pointer = context.AccessChain(context.TypePointer(storageClass, context.GetType(varType)), baseObj, indexes);
+                            break;
+                    }
+                    break;
+
+                default:
+                    throw new InvalidOperationException($"Invalid storage kind {storageKind}.");
+            }
+
+            if (isStore)
+            {
+                context.Store(pointer, context.Get(varType, operation.GetSource(srcIndex)));
+                return OperationResult.Invalid;
+            }
+            else
+            {
+                var result = context.Load(context.GetType(varType), pointer);
+                return new OperationResult(varType, result);
+            }
+        }
+
+        private static SpvInstruction GetScalarInput(CodeGenContext context, IoVariable ioVariable)
+        {
+            (_, var varType) = IoMap.GetSpirvBuiltIn(ioVariable);
+            varType &= AggregateType.ElementTypeMask;
+
+            var ioDefinition = new IoDefinition(StorageKind.Input, ioVariable);
+
+            return context.Load(context.GetType(varType), context.Inputs[ioDefinition]);
+        }
+
         private static void GenerateStoreSharedSmallInt(CodeGenContext context, AstOperation operation, int bitSize)
         {
             var offset = context.Get(AggregateType.U32, operation.GetSource(0));
diff --git a/Ryujinx.Graphics.Shader/CodeGen/Spirv/IoMap.cs b/Ryujinx.Graphics.Shader/CodeGen/Spirv/IoMap.cs
new file mode 100644
index 00000000..d2ff0085
--- /dev/null
+++ b/Ryujinx.Graphics.Shader/CodeGen/Spirv/IoMap.cs
@@ -0,0 +1,86 @@
+using Ryujinx.Graphics.Shader.IntermediateRepresentation;
+using Ryujinx.Graphics.Shader.Translation;
+using static Spv.Specification;
+
+namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
+{
+    static class IoMap
+    {
+        // At least 16 attributes are guaranteed by the spec.
+        private const int MaxAttributes = 16;
+
+        public static (BuiltIn, AggregateType) GetSpirvBuiltIn(IoVariable ioVariable)
+        {
+            return ioVariable switch
+            {
+                IoVariable.BaseInstance => (BuiltIn.BaseInstance, AggregateType.S32),
+                IoVariable.BaseVertex => (BuiltIn.BaseVertex, AggregateType.S32),
+                IoVariable.ClipDistance => (BuiltIn.ClipDistance, AggregateType.Array | AggregateType.FP32),
+                IoVariable.CtaId => (BuiltIn.WorkgroupId, AggregateType.Vector3 | AggregateType.U32),
+                IoVariable.DrawIndex => (BuiltIn.DrawIndex, AggregateType.S32),
+                IoVariable.FragmentCoord => (BuiltIn.FragCoord, AggregateType.Vector4 | AggregateType.FP32),
+                IoVariable.FragmentOutputDepth => (BuiltIn.FragDepth, AggregateType.FP32),
+                IoVariable.FrontFacing => (BuiltIn.FrontFacing, AggregateType.Bool),
+                IoVariable.InstanceId => (BuiltIn.InstanceId, AggregateType.S32),
+                IoVariable.InstanceIndex => (BuiltIn.InstanceIndex, AggregateType.S32),
+                IoVariable.InvocationId => (BuiltIn.InvocationId, AggregateType.S32),
+                IoVariable.Layer => (BuiltIn.Layer, AggregateType.S32),
+                IoVariable.PatchVertices => (BuiltIn.PatchVertices, AggregateType.S32),
+                IoVariable.PointCoord => (BuiltIn.PointCoord, AggregateType.Vector2 | AggregateType.FP32),
+                IoVariable.PointSize => (BuiltIn.PointSize, AggregateType.FP32),
+                IoVariable.Position => (BuiltIn.Position, AggregateType.Vector4 | AggregateType.FP32),
+                IoVariable.PrimitiveId => (BuiltIn.PrimitiveId, AggregateType.S32),
+                IoVariable.SubgroupEqMask => (BuiltIn.SubgroupEqMask, AggregateType.Vector4 | AggregateType.U32),
+                IoVariable.SubgroupGeMask => (BuiltIn.SubgroupGeMask, AggregateType.Vector4 | AggregateType.U32),
+                IoVariable.SubgroupGtMask => (BuiltIn.SubgroupGtMask, AggregateType.Vector4 | AggregateType.U32),
+                IoVariable.SubgroupLaneId => (BuiltIn.SubgroupLocalInvocationId, AggregateType.U32),
+                IoVariable.SubgroupLeMask => (BuiltIn.SubgroupLeMask, AggregateType.Vector4 | AggregateType.U32),
+                IoVariable.SubgroupLtMask => (BuiltIn.SubgroupLtMask, AggregateType.Vector4 | AggregateType.U32),
+                IoVariable.TessellationCoord => (BuiltIn.TessCoord, AggregateType.Vector3 | AggregateType.FP32),
+                IoVariable.TessellationLevelInner => (BuiltIn.TessLevelInner, AggregateType.Array | AggregateType.FP32),
+                IoVariable.TessellationLevelOuter => (BuiltIn.TessLevelOuter, AggregateType.Array | AggregateType.FP32),
+                IoVariable.ThreadId => (BuiltIn.LocalInvocationId, AggregateType.Vector3 | AggregateType.U32),
+                IoVariable.ThreadKill => (BuiltIn.HelperInvocation, AggregateType.Bool),
+                IoVariable.VertexId => (BuiltIn.VertexId, AggregateType.S32),
+                IoVariable.VertexIndex => (BuiltIn.VertexIndex, AggregateType.S32),
+                IoVariable.ViewportIndex => (BuiltIn.ViewportIndex, AggregateType.S32),
+                IoVariable.ViewportMask => (BuiltIn.ViewportMaskNV, AggregateType.Array | AggregateType.S32),
+                _ => (default, AggregateType.Invalid)
+            };
+        }
+
+        public static int GetSpirvBuiltInArrayLength(IoVariable ioVariable)
+        {
+            return ioVariable switch
+            {
+                IoVariable.ClipDistance => 8,
+                IoVariable.TessellationLevelInner => 2,
+                IoVariable.TessellationLevelOuter => 4,
+                IoVariable.ViewportMask => 1,
+                IoVariable.UserDefined => MaxAttributes,
+                _ => 1
+            };
+        }
+
+        public static bool IsPerVertex(IoVariable ioVariable, ShaderStage stage, bool isOutput)
+        {
+            switch (ioVariable)
+            {
+                case IoVariable.Layer:
+                case IoVariable.ViewportIndex:
+                case IoVariable.PointSize:
+                case IoVariable.Position:
+                case IoVariable.UserDefined:
+                case IoVariable.ClipDistance:
+                case IoVariable.PointCoord:
+                case IoVariable.ViewportMask:
+                return !isOutput &&
+                       (stage == ShaderStage.TessellationControl ||
+                       stage == ShaderStage.TessellationEvaluation ||
+                       stage == ShaderStage.Geometry);
+            }
+
+            return false;
+        }
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.Graphics.Shader/CodeGen/Spirv/ScalingHelpers.cs b/Ryujinx.Graphics.Shader/CodeGen/Spirv/ScalingHelpers.cs
index 8503771c..f6c218c6 100644
--- a/Ryujinx.Graphics.Shader/CodeGen/Spirv/ScalingHelpers.cs
+++ b/Ryujinx.Graphics.Shader/CodeGen/Spirv/ScalingHelpers.cs
@@ -156,7 +156,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
             var vectorFloat = context.ConvertSToF(vector2Type, vector);
             var vectorScaled = context.VectorTimesScalar(vector2Type, vectorFloat, scaleNegated);
 
-            var fragCoordPointer = context.Inputs[AttributeConsts.PositionX];
+            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);
 
diff --git a/Ryujinx.Graphics.Shader/CodeGen/Spirv/SpirvGenerator.cs b/Ryujinx.Graphics.Shader/CodeGen/Spirv/SpirvGenerator.cs
index ca823538..3e11a974 100644
--- a/Ryujinx.Graphics.Shader/CodeGen/Spirv/SpirvGenerator.cs
+++ b/Ryujinx.Graphics.Shader/CodeGen/Spirv/SpirvGenerator.cs
@@ -63,7 +63,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
 
             if (config.Stage == ShaderStage.Fragment)
             {
-                if (context.Info.Inputs.Contains(AttributeConsts.Layer))
+                if (context.Info.IoDefinitions.Contains(new IoDefinition(StorageKind.Input, IoVariable.Layer)))
                 {
                     context.AddCapability(Capability.Geometry);
                 }
@@ -93,13 +93,19 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
                 context.AddCapability(Capability.DrawParameters);
             }
 
-            Declarations.DeclareAll(context, info);
+            if (context.Info.IoDefinitions.Contains(new IoDefinition(StorageKind.Output, IoVariable.ViewportMask)))
+            {
+                context.AddExtension("SPV_NV_viewport_array2");
+                context.AddCapability(Capability.ShaderViewportMaskNV);
+            }
 
             if ((info.HelperFunctionsMask & NeedsInvocationIdMask) != 0)
             {
-                Declarations.DeclareInvocationId(context);
+                info.IoDefinitions.Add(new IoDefinition(StorageKind.Input, IoVariable.SubgroupLaneId));
             }
 
+            Declarations.DeclareAll(context, info);
+
             for (int funcIndex = 0; funcIndex < info.Functions.Count; funcIndex++)
             {
                 var function = info.Functions[funcIndex];
@@ -203,7 +209,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
 
                     if (context.Config.Options.TargetApi == TargetApi.Vulkan)
                     {
-                        // We invert the front face on Vulkan backend, so we need to do that here aswell.
+                        // We invert the front face on Vulkan backend, so we need to do that here as well.
                         tessCw = !tessCw;
                     }
 
@@ -250,7 +256,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
                         ? ExecutionMode.OriginUpperLeft
                         : ExecutionMode.OriginLowerLeft);
 
-                    if (context.Outputs.ContainsKey(AttributeConsts.FragmentOutputDepth))
+                    if (context.Info.IoDefinitions.Contains(new IoDefinition(StorageKind.Output, IoVariable.FragmentOutputDepth)))
                     {
                         context.AddExecutionMode(spvFunc, ExecutionMode.DepthReplacing);
                     }
@@ -389,21 +395,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
                         var source = context.Get(dest.VarType, assignment.Source);
                         context.Store(context.GetLocalPointer(dest), source);
                     }
-                    else if (dest.Type == OperandType.Attribute || dest.Type == OperandType.AttributePerPatch)
-                    {
-                        bool perPatch = dest.Type == OperandType.AttributePerPatch;
-
-                        if (AttributeInfo.Validate(context.Config, dest.Value, isOutAttr: true, perPatch))
-                        {
-                            AggregateType elemType;
-
-                            var elemPointer = perPatch
-                                ? context.GetAttributePerPatchElemPointer(dest.Value, true, out elemType)
-                                : context.GetAttributeElemPointer(dest.Value, true, null, out elemType);
-
-                            context.Store(elemPointer, context.Get(elemType, assignment.Source));
-                        }
-                    }
                     else if (dest.Type == OperandType.Argument)
                     {
                         var source = context.Get(dest.VarType, assignment.Source);
diff --git a/Ryujinx.Graphics.Shader/Decoders/Decoder.cs b/Ryujinx.Graphics.Shader/Decoders/Decoder.cs
index 380c425e..c619b9bb 100644
--- a/Ryujinx.Graphics.Shader/Decoders/Decoder.cs
+++ b/Ryujinx.Graphics.Shader/Decoders/Decoder.cs
@@ -295,10 +295,12 @@ namespace Ryujinx.Graphics.Shader.Decoders
                 if (isStore)
                 {
                     config.SetAllOutputUserAttributes();
+                    config.SetUsedFeature(FeatureFlags.OaIndexing);
                 }
                 else
                 {
                     config.SetAllInputUserAttributes();
+                    config.SetUsedFeature(FeatureFlags.IaIndexing);
                 }
             }
             else
@@ -340,7 +342,8 @@ namespace Ryujinx.Graphics.Shader.Decoders
                     }
 
                     if (!isStore &&
-                        ((attr >= AttributeConsts.FrontColorDiffuseR && attr < AttributeConsts.ClipDistance0) ||
+                        (attr == AttributeConsts.FogCoord ||
+                        (attr >= AttributeConsts.FrontColorDiffuseR && attr < AttributeConsts.ClipDistance0) ||
                         (attr >= AttributeConsts.TexCoordBase && attr < AttributeConsts.TexCoordEnd)))
                     {
                         config.SetUsedFeature(FeatureFlags.FixedFuncAttr);
diff --git a/Ryujinx.Graphics.Shader/IGpuAccessor.cs b/Ryujinx.Graphics.Shader/IGpuAccessor.cs
index bc5e67c3..2207156c 100644
--- a/Ryujinx.Graphics.Shader/IGpuAccessor.cs
+++ b/Ryujinx.Graphics.Shader/IGpuAccessor.cs
@@ -305,9 +305,9 @@ namespace Ryujinx.Graphics.Shader
         }
 
         /// <summary>
-        /// Queries host support for writes to Layer from vertex or tessellation shader stages.
+        /// Queries host support for writes to the layer from vertex or tessellation shader stages.
         /// </summary>
-        /// <returns>True if writes to layer from vertex or tessellation are supported, false otherwise</returns>
+        /// <returns>True if writes to the layer from vertex or tessellation are supported, false otherwise</returns>
         bool QueryHostSupportsLayerVertexTessellation()
         {
             return true;
@@ -350,10 +350,19 @@ namespace Ryujinx.Graphics.Shader
         }
 
         /// <summary>
-        /// Queries host GPU shader viewport index output support.
+        /// Queries host support for writes to the viewport index from vertex or tessellation shader stages.
         /// </summary>
-        /// <returns>True if the GPU and driver supports shader viewport index output, false otherwise</returns>
-        bool QueryHostSupportsViewportIndex()
+        /// <returns>True if writes to the viewport index from vertex or tessellation are supported, false otherwise</returns>
+        bool QueryHostSupportsViewportIndexVertexTessellation()
+        {
+            return true;
+        }
+
+        /// <summary>
+        /// Queries host GPU shader viewport mask output support.
+        /// </summary>
+        /// <returns>True if the GPU and driver supports shader viewport mask output, false otherwise</returns>
+        bool QueryHostSupportsViewportMask()
         {
             return true;
         }
diff --git a/Ryujinx.Graphics.Shader/Instructions/AttributeMap.cs b/Ryujinx.Graphics.Shader/Instructions/AttributeMap.cs
new file mode 100644
index 00000000..562fb8d5
--- /dev/null
+++ b/Ryujinx.Graphics.Shader/Instructions/AttributeMap.cs
@@ -0,0 +1,351 @@
+using Ryujinx.Graphics.Shader.IntermediateRepresentation;
+using Ryujinx.Graphics.Shader.Translation;
+using System.Collections.Generic;
+
+using static Ryujinx.Graphics.Shader.IntermediateRepresentation.OperandHelper;
+
+namespace Ryujinx.Graphics.Shader.Instructions
+{
+    static class AttributeMap
+    {
+        private enum StagesMask : byte
+        {
+            None = 0,
+            Compute = 1 << (int)ShaderStage.Compute,
+            Vertex = 1 << (int)ShaderStage.Vertex,
+            TessellationControl = 1 << (int)ShaderStage.TessellationControl,
+            TessellationEvaluation = 1 << (int)ShaderStage.TessellationEvaluation,
+            Geometry = 1 << (int)ShaderStage.Geometry,
+            Fragment = 1 << (int)ShaderStage.Fragment,
+
+            Tessellation = TessellationControl | TessellationEvaluation,
+            VertexTessellationGeometry = Vertex | Tessellation | Geometry,
+            TessellationGeometryFragment = Tessellation | Geometry | Fragment,
+            AllGraphics = Vertex | Tessellation | Geometry | Fragment
+        }
+
+        private struct AttributeEntry
+        {
+            public int BaseOffset { get; }
+            public AggregateType Type { get; }
+            public IoVariable IoVariable { get; }
+            public StagesMask InputMask { get; }
+            public StagesMask OutputMask { get; }
+
+            public AttributeEntry(
+                int baseOffset,
+                AggregateType type,
+                IoVariable ioVariable,
+                StagesMask inputMask,
+                StagesMask outputMask)
+            {
+                BaseOffset = baseOffset;
+                Type = type;
+                IoVariable = ioVariable;
+                InputMask = inputMask;
+                OutputMask = outputMask;
+            }
+        }
+
+        private static readonly IReadOnlyDictionary<int, AttributeEntry> _attributes;
+        private static readonly IReadOnlyDictionary<int, AttributeEntry> _attributesPerPatch;
+
+        static AttributeMap()
+        {
+            _attributes = CreateMap();
+            _attributesPerPatch = CreatePerPatchMap();
+        }
+
+        private static IReadOnlyDictionary<int, AttributeEntry> CreateMap()
+        {
+            var map = new Dictionary<int, AttributeEntry>();
+
+            Add(map, 0x060, AggregateType.S32, IoVariable.PrimitiveId, StagesMask.TessellationGeometryFragment, StagesMask.Geometry);
+            Add(map, 0x064, AggregateType.S32, IoVariable.Layer, StagesMask.Fragment, StagesMask.VertexTessellationGeometry);
+            Add(map, 0x068, AggregateType.S32, IoVariable.ViewportIndex, StagesMask.Fragment, StagesMask.VertexTessellationGeometry);
+            Add(map, 0x06c, AggregateType.FP32, IoVariable.PointSize, StagesMask.None, StagesMask.VertexTessellationGeometry);
+            Add(map, 0x070, AggregateType.Vector4 | AggregateType.FP32, IoVariable.Position, StagesMask.TessellationGeometryFragment, StagesMask.VertexTessellationGeometry);
+            Add(map, 0x080, AggregateType.Vector4 | AggregateType.FP32, IoVariable.UserDefined, StagesMask.AllGraphics, StagesMask.VertexTessellationGeometry, 32);
+            Add(map, 0x280, AggregateType.Vector4 | AggregateType.FP32, IoVariable.FrontColorDiffuse, StagesMask.TessellationGeometryFragment, StagesMask.VertexTessellationGeometry);
+            Add(map, 0x290, AggregateType.Vector4 | AggregateType.FP32, IoVariable.FrontColorSpecular, StagesMask.TessellationGeometryFragment, StagesMask.VertexTessellationGeometry);
+            Add(map, 0x2a0, AggregateType.Vector4 | AggregateType.FP32, IoVariable.BackColorDiffuse, StagesMask.TessellationGeometryFragment, StagesMask.VertexTessellationGeometry);
+            Add(map, 0x2b0, AggregateType.Vector4 | AggregateType.FP32, IoVariable.BackColorSpecular, StagesMask.TessellationGeometryFragment, StagesMask.VertexTessellationGeometry);
+            Add(map, 0x2c0, AggregateType.Array | AggregateType.FP32, IoVariable.ClipDistance, StagesMask.TessellationGeometryFragment, StagesMask.VertexTessellationGeometry, 8);
+            Add(map, 0x2e0, AggregateType.Vector2 | AggregateType.FP32, IoVariable.PointCoord, StagesMask.Fragment, StagesMask.None);
+            Add(map, 0x2e8, AggregateType.FP32, IoVariable.FogCoord, StagesMask.TessellationGeometryFragment, StagesMask.VertexTessellationGeometry);
+            Add(map, 0x2f0, AggregateType.Vector2 | AggregateType.FP32, IoVariable.TessellationCoord, StagesMask.TessellationEvaluation, StagesMask.None);
+            Add(map, 0x2f8, AggregateType.S32, IoVariable.InstanceId, StagesMask.Vertex, StagesMask.None);
+            Add(map, 0x2fc, AggregateType.S32, IoVariable.VertexId, StagesMask.Vertex, StagesMask.None);
+            Add(map, 0x300, AggregateType.Vector4 | AggregateType.FP32, IoVariable.TextureCoord, StagesMask.TessellationGeometryFragment, StagesMask.VertexTessellationGeometry);
+            Add(map, 0x3a0, AggregateType.Array | AggregateType.S32, IoVariable.ViewportMask, StagesMask.Fragment, StagesMask.VertexTessellationGeometry);
+            Add(map, 0x3fc, AggregateType.Bool, IoVariable.FrontFacing, StagesMask.Fragment, StagesMask.None);
+
+            return map;
+        }
+
+        private static IReadOnlyDictionary<int, AttributeEntry> CreatePerPatchMap()
+        {
+            var map = new Dictionary<int, AttributeEntry>();
+
+            Add(map, 0x000, AggregateType.Vector4 | AggregateType.FP32, IoVariable.TessellationLevelOuter, StagesMask.TessellationEvaluation, StagesMask.TessellationControl);
+            Add(map, 0x010, AggregateType.Vector2 | AggregateType.FP32, IoVariable.TessellationLevelInner, StagesMask.TessellationEvaluation, StagesMask.TessellationControl);
+            Add(map, 0x018, AggregateType.Vector4 | AggregateType.FP32, IoVariable.UserDefined, StagesMask.TessellationEvaluation, StagesMask.TessellationControl, 31, 0x200);
+
+            return map;
+        }
+
+        private static void Add(
+            Dictionary<int, AttributeEntry> attributes,
+            int offset,
+            AggregateType type,
+            IoVariable ioVariable,
+            StagesMask inputMask,
+            StagesMask outputMask,
+            int count = 1,
+            int upperBound = 0x400)
+        {
+            int baseOffset = offset;
+
+            int elementsCount = GetElementCount(type);
+
+            for (int index = 0; index < count; index++)
+            {
+                for (int elementIndex = 0; elementIndex < elementsCount; elementIndex++)
+                {
+                    attributes.Add(offset, new AttributeEntry(baseOffset, type, ioVariable, inputMask, outputMask));
+
+                    offset += 4;
+
+                    if (offset >= upperBound)
+                    {
+                        return;
+                    }
+                }
+            }
+        }
+
+        public static Operand GenerateAttributeLoad(EmitterContext context, Operand primVertex, int offset, bool isOutput, bool isPerPatch)
+        {
+            if (!(isPerPatch ? _attributesPerPatch : _attributes).TryGetValue(offset, out AttributeEntry entry))
+            {
+                context.Config.GpuAccessor.Log($"Attribute offset 0x{offset:X} is not valid.");
+                return Const(0);
+            }
+
+            StagesMask validUseMask = isOutput ? entry.OutputMask : entry.InputMask;
+
+            if (((StagesMask)(1 << (int)context.Config.Stage) & validUseMask) == StagesMask.None)
+            {
+                context.Config.GpuAccessor.Log($"Attribute offset 0x{offset:X} ({entry.IoVariable}) is not valid for stage {context.Config.Stage}.");
+                return Const(0);
+            }
+
+            if (!IsSupportedByHost(context.Config.GpuAccessor, context.Config.Stage, entry.IoVariable))
+            {
+                context.Config.GpuAccessor.Log($"Attribute offset 0x{offset:X} ({entry.IoVariable}) is not supported by the host for stage {context.Config.Stage}.");
+                return Const(0);
+            }
+
+            if (HasInvocationId(context.Config.Stage, isOutput) && !isPerPatch)
+            {
+                primVertex = context.Load(StorageKind.Input, IoVariable.InvocationId);
+            }
+
+            int innerOffset = offset - entry.BaseOffset;
+            int innerIndex = innerOffset / 4;
+
+            StorageKind storageKind = isPerPatch
+                ? (isOutput ? StorageKind.OutputPerPatch : StorageKind.InputPerPatch)
+                : (isOutput ? StorageKind.Output : StorageKind.Input);
+            IoVariable ioVariable = GetIoVariable(context.Config.Stage, in entry);
+            AggregateType type = GetType(context.Config, isOutput, innerIndex, in entry);
+            int elementCount = GetElementCount(type);
+
+            bool isArray = type.HasFlag(AggregateType.Array);
+            bool hasArrayIndex = isArray || context.Config.HasPerLocationInputOrOutput(ioVariable, isOutput);
+
+            bool hasElementIndex = elementCount > 1;
+
+            if (hasArrayIndex && hasElementIndex)
+            {
+                int arrayIndex = innerIndex / elementCount;
+                int elementIndex = innerIndex - (arrayIndex * elementCount);
+
+                return primVertex == null || isArray
+                    ? context.Load(storageKind, ioVariable, primVertex, Const(arrayIndex), Const(elementIndex))
+                    : context.Load(storageKind, ioVariable, Const(arrayIndex), primVertex, Const(elementIndex));
+            }
+            else if (hasArrayIndex || hasElementIndex)
+            {
+                return primVertex == null || isArray || !hasArrayIndex
+                    ? context.Load(storageKind, ioVariable, primVertex, Const(innerIndex))
+                    : context.Load(storageKind, ioVariable, Const(innerIndex), primVertex);
+            }
+            else
+            {
+                return context.Load(storageKind, ioVariable, primVertex);
+            }
+        }
+
+        public static void GenerateAttributeStore(EmitterContext context, int offset, bool isPerPatch, Operand value)
+        {
+            if (!(isPerPatch ? _attributesPerPatch : _attributes).TryGetValue(offset, out AttributeEntry entry))
+            {
+                context.Config.GpuAccessor.Log($"Attribute offset 0x{offset:X} is not valid.");
+                return;
+            }
+
+            if (((StagesMask)(1 << (int)context.Config.Stage) & entry.OutputMask) == StagesMask.None)
+            {
+                context.Config.GpuAccessor.Log($"Attribute offset 0x{offset:X} ({entry.IoVariable}) is not valid for stage {context.Config.Stage}.");
+                return;
+            }
+
+            if (!IsSupportedByHost(context.Config.GpuAccessor, context.Config.Stage, entry.IoVariable))
+            {
+                context.Config.GpuAccessor.Log($"Attribute offset 0x{offset:X} ({entry.IoVariable}) is not supported by the host for stage {context.Config.Stage}.");
+                return;
+            }
+
+            Operand invocationId = null;
+
+            if (HasInvocationId(context.Config.Stage, isOutput: true) && !isPerPatch)
+            {
+                invocationId = context.Load(StorageKind.Input, IoVariable.InvocationId);
+            }
+
+            int innerOffset = offset - entry.BaseOffset;
+            int innerIndex = innerOffset / 4;
+
+            StorageKind storageKind = isPerPatch ? StorageKind.OutputPerPatch : StorageKind.Output;
+            IoVariable ioVariable = GetIoVariable(context.Config.Stage, in entry);
+            AggregateType type = GetType(context.Config, isOutput: true, innerIndex, in entry);
+            int elementCount = GetElementCount(type);
+
+            bool isArray = type.HasFlag(AggregateType.Array);
+            bool hasArrayIndex = isArray || context.Config.HasPerLocationInputOrOutput(ioVariable, isOutput: true);
+
+            bool hasElementIndex = elementCount > 1;
+
+            if (hasArrayIndex && hasElementIndex)
+            {
+                int arrayIndex = innerIndex / elementCount;
+                int elementIndex = innerIndex - (arrayIndex * elementCount);
+
+                if (invocationId == null || isArray)
+                {
+                    context.Store(storageKind, ioVariable, invocationId, Const(arrayIndex), Const(elementIndex), value);
+                }
+                else
+                {
+                    context.Store(storageKind, ioVariable, Const(arrayIndex), invocationId, Const(elementIndex), value);
+                }
+            }
+            else if (hasArrayIndex || hasElementIndex)
+            {
+                if (invocationId == null || isArray || !hasArrayIndex)
+                {
+                    context.Store(storageKind, ioVariable, invocationId, Const(innerIndex), value);
+                }
+                else
+                {
+                    context.Store(storageKind, ioVariable, Const(innerIndex), invocationId, value);
+                }
+            }
+            else
+            {
+                context.Store(storageKind, ioVariable, invocationId, value);
+            }
+        }
+
+        private static bool IsSupportedByHost(IGpuAccessor gpuAccessor, ShaderStage stage, IoVariable ioVariable)
+        {
+            if (ioVariable == IoVariable.ViewportIndex && stage != ShaderStage.Geometry && stage != ShaderStage.Fragment)
+            {
+                return gpuAccessor.QueryHostSupportsViewportIndexVertexTessellation();
+            }
+            else if (ioVariable == IoVariable.ViewportMask)
+            {
+                return gpuAccessor.QueryHostSupportsViewportMask();
+            }
+
+            return true;
+        }
+
+        public static IoVariable GetIoVariable(ShaderConfig config, int offset, out int location)
+        {
+            location = 0;
+
+            if (!_attributes.TryGetValue(offset, out AttributeEntry entry))
+            {
+                return IoVariable.Invalid;
+            }
+
+            if (((StagesMask)(1 << (int)config.Stage) & entry.OutputMask) == StagesMask.None)
+            {
+                return IoVariable.Invalid;
+            }
+
+            if (config.HasPerLocationInputOrOutput(entry.IoVariable, isOutput: true))
+            {
+                location = (offset - entry.BaseOffset) / 16;
+            }
+
+            return GetIoVariable(config.Stage, in entry);
+        }
+
+        private static IoVariable GetIoVariable(ShaderStage stage, in AttributeEntry entry)
+        {
+            if (entry.IoVariable == IoVariable.Position && stage == ShaderStage.Fragment)
+            {
+                return IoVariable.FragmentCoord;
+            }
+
+            return entry.IoVariable;
+        }
+
+        private static AggregateType GetType(ShaderConfig config, bool isOutput, int innerIndex, in AttributeEntry entry)
+        {
+            AggregateType type = entry.Type;
+
+            if (entry.IoVariable == IoVariable.UserDefined)
+            {
+                type = config.GetUserDefinedType(innerIndex / 4, isOutput);
+            }
+            else if (entry.IoVariable == IoVariable.FragmentOutputColor)
+            {
+                type = config.GetFragmentOutputColorType(innerIndex / 4);
+            }
+
+            return type;
+        }
+
+        public static bool HasPrimitiveVertex(ShaderStage stage, bool isOutput)
+        {
+            if (isOutput)
+            {
+                return false;
+            }
+
+            return stage == ShaderStage.TessellationControl ||
+                   stage == ShaderStage.TessellationEvaluation ||
+                   stage == ShaderStage.Geometry;
+        }
+
+        public static bool HasInvocationId(ShaderStage stage, bool isOutput)
+        {
+            return isOutput && stage == ShaderStage.TessellationControl;
+        }
+
+        private static int GetElementCount(AggregateType type)
+        {
+            return (type & AggregateType.ElementCountMask) switch
+            {
+                AggregateType.Vector2 => 2,
+                AggregateType.Vector3 => 3,
+                AggregateType.Vector4 => 4,
+                _ => 1
+            };
+        }
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.Graphics.Shader/Instructions/InstEmitAttribute.cs b/Ryujinx.Graphics.Shader/Instructions/InstEmitAttribute.cs
index 9f9ac141..1df38761 100644
--- a/Ryujinx.Graphics.Shader/Instructions/InstEmitAttribute.cs
+++ b/Ryujinx.Graphics.Shader/Instructions/InstEmitAttribute.cs
@@ -20,7 +20,16 @@ namespace Ryujinx.Graphics.Shader.Instructions
         {
             InstAld op = context.GetOp<InstAld>();
 
-            Operand primVertex = context.Copy(GetSrcReg(context, op.SrcB));
+            // Some of those attributes are per invocation,
+            // so we should ignore any primitive vertex indexing for those.
+            bool hasPrimitiveVertex = AttributeMap.HasPrimitiveVertex(context.Config.Stage, op.O) && !op.P;
+
+            if (!op.Phys)
+            {
+                hasPrimitiveVertex &= HasPrimitiveVertex(op.Imm11);
+            }
+
+            Operand primVertex = hasPrimitiveVertex ? context.Copy(GetSrcReg(context, op.SrcB)) : null;
 
             for (int index = 0; index < (int)op.AlSize + 1; index++)
             {
@@ -33,12 +42,13 @@ namespace Ryujinx.Graphics.Shader.Instructions
 
                 if (op.Phys)
                 {
-                    Operand userAttrOffset = context.ISubtract(GetSrcReg(context, op.SrcA), Const(AttributeConsts.UserAttributeBase));
-                    Operand userAttrIndex = context.ShiftRightU32(userAttrOffset, Const(2));
+                    Operand offset = context.ISubtract(GetSrcReg(context, op.SrcA), Const(AttributeConsts.UserAttributeBase));
+                    Operand vecIndex = context.ShiftRightU32(offset, Const(4));
+                    Operand elemIndex = context.BitwiseAnd(context.ShiftRightU32(offset, Const(2)), Const(3));
 
-                    context.Copy(Register(rd), context.LoadAttribute(Const(AttributeConsts.UserAttributeBase), userAttrIndex, primVertex));
+                    StorageKind storageKind = op.O ? StorageKind.Output : StorageKind.Input;
 
-                    context.Config.SetUsedFeature(FeatureFlags.IaIndexing);
+                    context.Copy(Register(rd), context.Load(storageKind, IoVariable.UserDefined, primVertex, vecIndex, elemIndex));
                 }
                 else if (op.SrcB == RegisterConsts.RegisterZeroIndex || op.P)
                 {
@@ -46,14 +56,16 @@ namespace Ryujinx.Graphics.Shader.Instructions
 
                     context.FlagAttributeRead(offset);
 
-                    if (op.O && CanLoadOutput(offset))
+                    bool isOutput = op.O && CanLoadOutput(offset);
+
+                    if (!op.P && !isOutput && TryConvertIdToIndexForVulkan(context, offset, out Operand value))
                     {
-                        offset |= AttributeConsts.LoadOutputMask;
+                        context.Copy(Register(rd), value);
+                    }
+                    else
+                    {
+                        context.Copy(Register(rd), AttributeMap.GenerateAttributeLoad(context, primVertex, offset, isOutput, op.P));
                     }
-
-                    Operand src = op.P ? AttributePerPatch(offset) : CreateInputAttribute(context, offset);
-
-                    context.Copy(Register(rd), src);
                 }
                 else
                 {
@@ -61,14 +73,9 @@ namespace Ryujinx.Graphics.Shader.Instructions
 
                     context.FlagAttributeRead(offset);
 
-                    if (op.O && CanLoadOutput(offset))
-                    {
-                        offset |= AttributeConsts.LoadOutputMask;
-                    }
-
-                    Operand src = Const(offset);
+                    bool isOutput = op.O && CanLoadOutput(offset);
 
-                    context.Copy(Register(rd), context.LoadAttribute(src, Const(0), primVertex));
+                    context.Copy(Register(rd), AttributeMap.GenerateAttributeLoad(context, primVertex, offset, isOutput, false));
                 }
             }
         }
@@ -88,12 +95,14 @@ namespace Ryujinx.Graphics.Shader.Instructions
 
                 if (op.Phys)
                 {
-                    Operand userAttrOffset = context.ISubtract(GetSrcReg(context, op.SrcA), Const(AttributeConsts.UserAttributeBase));
-                    Operand userAttrIndex = context.ShiftRightU32(userAttrOffset, Const(2));
-
-                    context.StoreAttribute(Const(AttributeConsts.UserAttributeBase), userAttrIndex, Register(rd));
-
-                    context.Config.SetUsedFeature(FeatureFlags.OaIndexing);
+                    Operand offset = context.ISubtract(GetSrcReg(context, op.SrcA), Const(AttributeConsts.UserAttributeBase));
+                    Operand vecIndex = context.ShiftRightU32(offset, Const(4));
+                    Operand elemIndex = context.BitwiseAnd(context.ShiftRightU32(offset, Const(2)), Const(3));
+                    Operand invocationId = AttributeMap.HasInvocationId(context.Config.Stage, isOutput: true)
+                        ? context.Load(StorageKind.Input, IoVariable.InvocationId)
+                        : null;
+
+                    context.Store(StorageKind.Output, IoVariable.UserDefined, invocationId, vecIndex, elemIndex, Register(rd));
                 }
                 else
                 {
@@ -110,9 +119,7 @@ namespace Ryujinx.Graphics.Shader.Instructions
 
                     context.FlagAttributeWritten(offset);
 
-                    Operand dest = op.P ? AttributePerPatch(offset) : Attribute(offset);
-
-                    context.Copy(dest, Register(rd));
+                    AttributeMap.GenerateAttributeStore(context, offset, op.P, Register(rd));
                 }
             }
         }
@@ -129,13 +136,12 @@ namespace Ryujinx.Graphics.Shader.Instructions
 
             if (op.Idx)
             {
-                Operand userAttrOffset = context.ISubtract(GetSrcReg(context, op.SrcA), Const(AttributeConsts.UserAttributeBase));
-                Operand userAttrIndex = context.ShiftRightU32(userAttrOffset, Const(2));
+                Operand offset = context.ISubtract(GetSrcReg(context, op.SrcA), Const(AttributeConsts.UserAttributeBase));
+                Operand vecIndex = context.ShiftRightU32(offset, Const(4));
+                Operand elemIndex = context.BitwiseAnd(context.ShiftRightU32(offset, Const(2)), Const(3));
 
-                res = context.LoadAttribute(Const(AttributeConsts.UserAttributeBase), userAttrIndex, Const(0));
-                res = context.FPMultiply(res, Attribute(AttributeConsts.PositionW));
-
-                context.Config.SetUsedFeature(FeatureFlags.IaIndexing);
+                res = context.Load(StorageKind.Input, IoVariable.UserDefined, null, vecIndex, elemIndex);
+                res = context.FPMultiply(res, context.Load(StorageKind.Input, IoVariable.FragmentCoord, null, Const(3)));
             }
             else
             {
@@ -147,9 +153,21 @@ namespace Ryujinx.Graphics.Shader.Instructions
 
                     if (context.Config.ImapTypes[index].GetFirstUsedType() == PixelImap.Perspective)
                     {
-                        res = context.FPMultiply(res, Attribute(AttributeConsts.PositionW));
+                        res = context.FPMultiply(res, context.Load(StorageKind.Input, IoVariable.FragmentCoord, null, Const(3)));
                     }
                 }
+                else if (op.Imm10 == AttributeConsts.PositionX || op.Imm10 == AttributeConsts.PositionY)
+                {
+                    // FragCoord X/Y must be divided by the render target scale, if resolution scaling is active,
+                    // because the shader code is not expecting scaled values.
+                    res = context.FPDivide(res, context.Load(StorageKind.Input, IoVariable.SupportBlockRenderScale, null, Const(0)));
+                }
+                else if (op.Imm10 == AttributeConsts.FrontFacing && context.Config.GpuAccessor.QueryHostHasFrontFacingBug())
+                {
+                    // gl_FrontFacing sometimes has incorrect (flipped) values depending how it is accessed on Intel GPUs.
+                    // This weird trick makes it behave.
+                    res = context.ICompareLess(context.INegate(context.IConvertS32ToFP32(res)), Const(0));
+                }
             }
 
             if (op.IpaOp == IpaOp.Multiply && !isFixedFunc)
@@ -216,17 +234,17 @@ namespace Ryujinx.Graphics.Shader.Instructions
 
                     if (tempXLocal != null)
                     {
-                        context.Copy(Attribute(AttributeConsts.PositionX), tempXLocal);
+                        context.Copy(context.Load(StorageKind.Input, IoVariable.Position, null, Const(0)), tempXLocal);
                     }
 
                     if (tempYLocal != null)
                     {
-                        context.Copy(Attribute(AttributeConsts.PositionY), tempYLocal);
+                        context.Copy(context.Load(StorageKind.Input, IoVariable.Position, null, Const(1)), tempYLocal);
                     }
 
                     if (tempZLocal != null)
                     {
-                        context.Copy(Attribute(AttributeConsts.PositionZ), tempZLocal);
+                        context.Copy(context.Load(StorageKind.Input, IoVariable.Position, null, Const(2)), tempZLocal);
                     }
                 }
                 else
@@ -241,6 +259,13 @@ namespace Ryujinx.Graphics.Shader.Instructions
             }
         }
 
+        private static bool HasPrimitiveVertex(int attr)
+        {
+            return attr != AttributeConsts.PrimitiveId &&
+                   attr != AttributeConsts.TessCoordX &&
+                   attr != AttributeConsts.TessCoordY;
+        }
+
         private static bool CanLoadOutput(int attr)
         {
             return attr != AttributeConsts.TessCoordX && attr != AttributeConsts.TessCoordY;
@@ -252,13 +277,13 @@ namespace Ryujinx.Graphics.Shader.Instructions
             {
                 // TODO: If two sided rendering is enabled, then this should return
                 // FrontColor if the fragment is front facing, and back color otherwise.
-                int index = (attr - AttributeConsts.FrontColorDiffuseR) >> 4;
-                int userAttrIndex = context.Config.GetFreeUserAttribute(isOutput: false, index);
-                Operand frontAttr = Attribute(AttributeConsts.UserAttributeBase + userAttrIndex * 16 + (attr & 0xf));
-
-                context.Config.SetInputUserAttributeFixedFunc(userAttrIndex);
-
-                selectedAttr = frontAttr;
+                selectedAttr = GenerateIpaLoad(context, FixedFuncToUserAttribute(context.Config, attr, isOutput: false));
+                return true;
+            }
+            else if (attr == AttributeConsts.FogCoord)
+            {
+                // TODO: We likely need to emulate the fixed-function functionality for FogCoord here.
+                selectedAttr = GenerateIpaLoad(context, FixedFuncToUserAttribute(context.Config, attr, isOutput: false));
                 return true;
             }
             else if (attr >= AttributeConsts.BackColorDiffuseR && attr < AttributeConsts.ClipDistance0)
@@ -268,14 +293,19 @@ namespace Ryujinx.Graphics.Shader.Instructions
             }
             else if (attr >= AttributeConsts.TexCoordBase && attr < AttributeConsts.TexCoordEnd)
             {
-                selectedAttr = Attribute(FixedFuncToUserAttribute(context.Config, attr, AttributeConsts.TexCoordBase, 4, isOutput: false));
+                selectedAttr = GenerateIpaLoad(context, FixedFuncToUserAttribute(context.Config, attr, isOutput: false));
                 return true;
             }
 
-            selectedAttr = Attribute(attr);
+            selectedAttr = GenerateIpaLoad(context, attr);
             return false;
         }
 
+        private static Operand GenerateIpaLoad(EmitterContext context, int offset)
+        {
+            return AttributeMap.GenerateAttributeLoad(context, null, offset, isOutput: false, isPerPatch: false);
+        }
+
         private static int FixedFuncToUserAttribute(ShaderConfig config, int attr, bool isOutput)
         {
             bool supportsLayerFromVertexOrTess = config.GpuAccessor.QueryHostSupportsLayerVertexTessellation();
@@ -286,13 +316,17 @@ namespace Ryujinx.Graphics.Shader.Instructions
                 attr = FixedFuncToUserAttribute(config, attr, AttributeConsts.Layer, 0, isOutput);
                 config.SetLayerOutputAttribute(attr);
             }
+            else if (attr == AttributeConsts.FogCoord)
+            {
+                attr = FixedFuncToUserAttribute(config, attr, AttributeConsts.FogCoord, fixedStartAttr, isOutput);
+            }
             else if (attr >= AttributeConsts.FrontColorDiffuseR && attr < AttributeConsts.ClipDistance0)
             {
-                attr = FixedFuncToUserAttribute(config, attr, AttributeConsts.FrontColorDiffuseR, fixedStartAttr, isOutput);
+                attr = FixedFuncToUserAttribute(config, attr, AttributeConsts.FrontColorDiffuseR, fixedStartAttr + 1, isOutput);
             }
             else if (attr >= AttributeConsts.TexCoordBase && attr < AttributeConsts.TexCoordEnd)
             {
-                attr = FixedFuncToUserAttribute(config, attr, AttributeConsts.TexCoordBase, fixedStartAttr + 4, isOutput);
+                attr = FixedFuncToUserAttribute(config, attr, AttributeConsts.TexCoordBase, fixedStartAttr + 5, isOutput);
             }
 
             return attr;
@@ -301,11 +335,10 @@ namespace Ryujinx.Graphics.Shader.Instructions
         private static int FixedFuncToUserAttribute(ShaderConfig config, int attr, int baseAttr, int baseIndex, bool isOutput)
         {
             int index = (attr - baseAttr) >> 4;
-            int userAttrIndex = config.GetFreeUserAttribute(isOutput, index);
+            int userAttrIndex = config.GetFreeUserAttribute(isOutput, baseIndex + index);
 
             if ((uint)userAttrIndex < Constants.MaxAttributes)
             {
-                userAttrIndex += baseIndex;
                 attr = AttributeConsts.UserAttributeBase + userAttrIndex * 16 + (attr & 0xf);
 
                 if (isOutput)
@@ -317,25 +350,34 @@ namespace Ryujinx.Graphics.Shader.Instructions
                     config.SetInputUserAttributeFixedFunc(userAttrIndex);
                 }
             }
+            else
+            {
+                config.GpuAccessor.Log($"No enough user attributes for fixed attribute offset 0x{attr:X}.");
+            }
 
             return attr;
         }
 
-        private static Operand CreateInputAttribute(EmitterContext context, int attr)
+        private static bool TryConvertIdToIndexForVulkan(EmitterContext context, int attr, out Operand value)
         {
             if (context.Config.Options.TargetApi == TargetApi.Vulkan)
             {
                 if (attr == AttributeConsts.InstanceId)
                 {
-                    return context.ISubtract(Attribute(AttributeConsts.InstanceIndex), Attribute(AttributeConsts.BaseInstance));
+                    value = context.ISubtract(
+                        context.Load(StorageKind.Input, IoVariable.InstanceIndex),
+                        context.Load(StorageKind.Input, IoVariable.BaseInstance));
+                    return true;
                 }
                 else if (attr == AttributeConsts.VertexId)
                 {
-                    return Attribute(AttributeConsts.VertexIndex);
+                    value = context.Load(StorageKind.Input, IoVariable.VertexIndex);
+                    return true;
                 }
             }
 
-            return Attribute(attr);
+            value = null;
+            return false;
         }
     }
 }
\ No newline at end of file
diff --git a/Ryujinx.Graphics.Shader/Instructions/InstEmitMemory.cs b/Ryujinx.Graphics.Shader/Instructions/InstEmitMemory.cs
index ceb76de1..c73c6b2a 100644
--- a/Ryujinx.Graphics.Shader/Instructions/InstEmitMemory.cs
+++ b/Ryujinx.Graphics.Shader/Instructions/InstEmitMemory.cs
@@ -25,7 +25,7 @@ namespace Ryujinx.Graphics.Shader.Instructions
 
             Operand value = GetSrcReg(context, op.SrcB);
 
-            Operand res = EmitAtomicOp(context, Instruction.MrGlobal, op.Op, op.Size, addrLow, addrHigh, value);
+            Operand res = EmitAtomicOp(context, StorageKind.GlobalMemory, op.Op, op.Size, addrLow, addrHigh, value);
 
             context.Copy(GetDest(op.Dest), res);
         }
@@ -50,7 +50,7 @@ namespace Ryujinx.Graphics.Shader.Instructions
                 _ => AtomSize.U32
             };
 
-            Operand res = EmitAtomicOp(context, Instruction.MrShared, op.AtomOp, size, offset, Const(0), value);
+            Operand res = EmitAtomicOp(context, StorageKind.SharedMemory, op.AtomOp, size, offset, Const(0), value);
 
             context.Copy(GetDest(op.Dest), res);
         }
@@ -130,7 +130,7 @@ namespace Ryujinx.Graphics.Shader.Instructions
 
             (Operand addrLow, Operand addrHigh) = Get40BitsAddress(context, new Register(op.SrcA, RegisterType.Gpr), op.E, op.Imm20);
 
-            EmitAtomicOp(context, Instruction.MrGlobal, (AtomOp)op.RedOp, op.RedSize, addrLow, addrHigh, GetDest(op.SrcB));
+            EmitAtomicOp(context, StorageKind.GlobalMemory, (AtomOp)op.RedOp, op.RedSize, addrLow, addrHigh, GetDest(op.SrcB));
         }
 
         public static void Stg(EmitterContext context)
@@ -156,7 +156,7 @@ namespace Ryujinx.Graphics.Shader.Instructions
 
         private static Operand EmitAtomicOp(
             EmitterContext context,
-            Instruction mr,
+            StorageKind storageKind,
             AtomOp op,
             AtomSize type,
             Operand addrLow,
@@ -170,7 +170,7 @@ namespace Ryujinx.Graphics.Shader.Instructions
                 case AtomOp.Add:
                     if (type == AtomSize.S32 || type == AtomSize.U32)
                     {
-                        res = context.AtomicAdd(mr, addrLow, addrHigh, value);
+                        res = context.AtomicAdd(storageKind, addrLow, addrHigh, value);
                     }
                     else
                     {
@@ -180,7 +180,7 @@ namespace Ryujinx.Graphics.Shader.Instructions
                 case AtomOp.And:
                     if (type == AtomSize.S32 || type == AtomSize.U32)
                     {
-                        res = context.AtomicAnd(mr, addrLow, addrHigh, value);
+                        res = context.AtomicAnd(storageKind, addrLow, addrHigh, value);
                     }
                     else
                     {
@@ -190,7 +190,7 @@ namespace Ryujinx.Graphics.Shader.Instructions
                 case AtomOp.Xor:
                     if (type == AtomSize.S32 || type == AtomSize.U32)
                     {
-                        res = context.AtomicXor(mr, addrLow, addrHigh, value);
+                        res = context.AtomicXor(storageKind, addrLow, addrHigh, value);
                     }
                     else
                     {
@@ -200,7 +200,7 @@ namespace Ryujinx.Graphics.Shader.Instructions
                 case AtomOp.Or:
                     if (type == AtomSize.S32 || type == AtomSize.U32)
                     {
-                        res = context.AtomicOr(mr, addrLow, addrHigh, value);
+                        res = context.AtomicOr(storageKind, addrLow, addrHigh, value);
                     }
                     else
                     {
@@ -210,11 +210,11 @@ namespace Ryujinx.Graphics.Shader.Instructions
                 case AtomOp.Max:
                     if (type == AtomSize.S32)
                     {
-                        res = context.AtomicMaxS32(mr, addrLow, addrHigh, value);
+                        res = context.AtomicMaxS32(storageKind, addrLow, addrHigh, value);
                     }
                     else if (type == AtomSize.U32)
                     {
-                        res = context.AtomicMaxU32(mr, addrLow, addrHigh, value);
+                        res = context.AtomicMaxU32(storageKind, addrLow, addrHigh, value);
                     }
                     else
                     {
@@ -224,11 +224,11 @@ namespace Ryujinx.Graphics.Shader.Instructions
                 case AtomOp.Min:
                     if (type == AtomSize.S32)
                     {
-                        res = context.AtomicMinS32(mr, addrLow, addrHigh, value);
+                        res = context.AtomicMinS32(storageKind, addrLow, addrHigh, value);
                     }
                     else if (type == AtomSize.U32)
                     {
-                        res = context.AtomicMinU32(mr, addrLow, addrHigh, value);
+                        res = context.AtomicMinU32(storageKind, addrLow, addrHigh, value);
                     }
                     else
                     {
diff --git a/Ryujinx.Graphics.Shader/Instructions/InstEmitMove.cs b/Ryujinx.Graphics.Shader/Instructions/InstEmitMove.cs
index 16b02f97..9992ac37 100644
--- a/Ryujinx.Graphics.Shader/Instructions/InstEmitMove.cs
+++ b/Ryujinx.Graphics.Shader/Instructions/InstEmitMove.cs
@@ -76,11 +76,11 @@ namespace Ryujinx.Graphics.Shader.Instructions
             switch (op.SReg)
             {
                 case SReg.LaneId:
-                    src = Attribute(AttributeConsts.LaneId);
+                    src = context.Load(StorageKind.Input, IoVariable.SubgroupLaneId);
                     break;
 
                 case SReg.InvocationId:
-                    src = Attribute(AttributeConsts.InvocationId);
+                    src = context.Load(StorageKind.Input, IoVariable.InvocationId);
                     break;
 
                 case SReg.YDirection:
@@ -88,7 +88,7 @@ namespace Ryujinx.Graphics.Shader.Instructions
                     break;
 
                 case SReg.ThreadKill:
-                    src = context.Config.Stage == ShaderStage.Fragment ? Attribute(AttributeConsts.ThreadKill) : Const(0);
+                    src = context.Config.Stage == ShaderStage.Fragment ? context.Load(StorageKind.Input, IoVariable.ThreadKill) : Const(0);
                     break;
 
                 case SReg.InvocationInfo:
@@ -101,7 +101,7 @@ namespace Ryujinx.Graphics.Shader.Instructions
                         if (context.Config.Stage == ShaderStage.TessellationControl ||
                             context.Config.Stage == ShaderStage.TessellationEvaluation)
                         {
-                            src = context.ShiftLeft(Attribute(AttributeConsts.PatchVerticesIn), Const(16));
+                            src = context.ShiftLeft(context.Load(StorageKind.Input, IoVariable.PatchVertices), Const(16));
                         }
                         else
                         {
@@ -115,9 +115,9 @@ namespace Ryujinx.Graphics.Shader.Instructions
                     break;
 
                 case SReg.TId:
-                    Operand tidX = Attribute(AttributeConsts.ThreadIdX);
-                    Operand tidY = Attribute(AttributeConsts.ThreadIdY);
-                    Operand tidZ = Attribute(AttributeConsts.ThreadIdZ);
+                    Operand tidX = context.Load(StorageKind.Input, IoVariable.ThreadId, null, Const(0));
+                    Operand tidY = context.Load(StorageKind.Input, IoVariable.ThreadId, null, Const(1));
+                    Operand tidZ = context.Load(StorageKind.Input, IoVariable.ThreadId, null, Const(2));
 
                     tidY = context.ShiftLeft(tidY, Const(16));
                     tidZ = context.ShiftLeft(tidZ, Const(26));
@@ -126,39 +126,39 @@ namespace Ryujinx.Graphics.Shader.Instructions
                     break;
 
                 case SReg.TIdX:
-                    src = Attribute(AttributeConsts.ThreadIdX);
+                    src = context.Load(StorageKind.Input, IoVariable.ThreadId, null, Const(0));
                     break;
                 case SReg.TIdY:
-                    src = Attribute(AttributeConsts.ThreadIdY);
+                    src = context.Load(StorageKind.Input, IoVariable.ThreadId, null, Const(1));
                     break;
                 case SReg.TIdZ:
-                    src = Attribute(AttributeConsts.ThreadIdZ);
+                    src = context.Load(StorageKind.Input, IoVariable.ThreadId, null, Const(2));
                     break;
 
                 case SReg.CtaIdX:
-                    src = Attribute(AttributeConsts.CtaIdX);
+                    src = context.Load(StorageKind.Input, IoVariable.CtaId, null, Const(0));
                     break;
                 case SReg.CtaIdY:
-                    src = Attribute(AttributeConsts.CtaIdY);
+                    src = context.Load(StorageKind.Input, IoVariable.CtaId, null, Const(1));
                     break;
                 case SReg.CtaIdZ:
-                    src = Attribute(AttributeConsts.CtaIdZ);
+                    src = context.Load(StorageKind.Input, IoVariable.CtaId, null, Const(2));
                     break;
 
                 case SReg.EqMask:
-                    src = Attribute(AttributeConsts.EqMask);
+                    src = context.Load(StorageKind.Input, IoVariable.SubgroupEqMask, null, Const(0));
                     break;
                 case SReg.LtMask:
-                    src = Attribute(AttributeConsts.LtMask);
+                    src = context.Load(StorageKind.Input, IoVariable.SubgroupLtMask, null, Const(0));
                     break;
                 case SReg.LeMask:
-                    src = Attribute(AttributeConsts.LeMask);
+                    src = context.Load(StorageKind.Input, IoVariable.SubgroupLeMask, null, Const(0));
                     break;
                 case SReg.GtMask:
-                    src = Attribute(AttributeConsts.GtMask);
+                    src = context.Load(StorageKind.Input, IoVariable.SubgroupGtMask, null, Const(0));
                     break;
                 case SReg.GeMask:
-                    src = Attribute(AttributeConsts.GeMask);
+                    src = context.Load(StorageKind.Input, IoVariable.SubgroupGeMask, null, Const(0));
                     break;
 
                 default:
diff --git a/Ryujinx.Graphics.Shader/IntermediateRepresentation/Instruction.cs b/Ryujinx.Graphics.Shader/IntermediateRepresentation/Instruction.cs
index aa9776bc..d7c4a961 100644
--- a/Ryujinx.Graphics.Shader/IntermediateRepresentation/Instruction.cs
+++ b/Ryujinx.Graphics.Shader/IntermediateRepresentation/Instruction.cs
@@ -78,7 +78,7 @@ namespace Ryujinx.Graphics.Shader.IntermediateRepresentation
         ImageStore,
         ImageAtomic,
         IsNan,
-        LoadAttribute,
+        Load,
         LoadConstant,
         LoadGlobal,
         LoadLocal,
@@ -116,7 +116,7 @@ namespace Ryujinx.Graphics.Shader.IntermediateRepresentation
         ShuffleXor,
         Sine,
         SquareRoot,
-        StoreAttribute,
+        Store,
         StoreGlobal,
         StoreGlobal16,
         StoreGlobal8,
@@ -144,13 +144,6 @@ namespace Ryujinx.Graphics.Shader.IntermediateRepresentation
         FP32 = 1 << 16,
         FP64 = 1 << 17,
 
-        MrShift = 18,
-
-        MrGlobal  = 0 << MrShift,
-        MrShared  = 1 << MrShift,
-        MrStorage = 2 << MrShift,
-        MrMask    = 3 << MrShift,
-
         Mask = 0xffff
     }
 
diff --git a/Ryujinx.Graphics.Shader/IntermediateRepresentation/IoVariable.cs b/Ryujinx.Graphics.Shader/IntermediateRepresentation/IoVariable.cs
new file mode 100644
index 00000000..a2163d14
--- /dev/null
+++ b/Ryujinx.Graphics.Shader/IntermediateRepresentation/IoVariable.cs
@@ -0,0 +1,51 @@
+namespace Ryujinx.Graphics.Shader.IntermediateRepresentation
+{
+    enum IoVariable
+    {
+        Invalid,
+
+        BackColorDiffuse,
+        BackColorSpecular,
+        BaseInstance,
+        BaseVertex,
+        ClipDistance,
+        CtaId,
+        DrawIndex,
+        FogCoord,
+        FragmentCoord,
+        FragmentOutputColor,
+        FragmentOutputDepth,
+        FragmentOutputIsBgra, // TODO: Remove and use constant buffer access.
+        FrontColorDiffuse,
+        FrontColorSpecular,
+        FrontFacing,
+        InstanceId,
+        InstanceIndex,
+        InvocationId,
+        Layer,
+        PatchVertices,
+        PointCoord,
+        PointSize,
+        Position,
+        PrimitiveId,
+        SubgroupEqMask,
+        SubgroupGeMask,
+        SubgroupGtMask,
+        SubgroupLaneId,
+        SubgroupLeMask,
+        SubgroupLtMask,
+        SupportBlockViewInverse, // TODO: Remove and use constant buffer access.
+        SupportBlockRenderScale, // TODO: Remove and use constant buffer access.
+        TessellationCoord,
+        TessellationLevelInner,
+        TessellationLevelOuter,
+        TextureCoord,
+        ThreadId,
+        ThreadKill,
+        UserDefined,
+        VertexId,
+        VertexIndex,
+        ViewportIndex,
+        ViewportMask
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.Graphics.Shader/IntermediateRepresentation/OperandHelper.cs b/Ryujinx.Graphics.Shader/IntermediateRepresentation/OperandHelper.cs
index 7fed861e..37c349e8 100644
--- a/Ryujinx.Graphics.Shader/IntermediateRepresentation/OperandHelper.cs
+++ b/Ryujinx.Graphics.Shader/IntermediateRepresentation/OperandHelper.cs
@@ -10,16 +10,6 @@ namespace Ryujinx.Graphics.Shader.IntermediateRepresentation
             return new Operand(OperandType.Argument, value);
         }
 
-        public static Operand Attribute(int value)
-        {
-            return new Operand(OperandType.Attribute, value);
-        }
-
-        public static Operand AttributePerPatch(int value)
-        {
-            return new Operand(OperandType.AttributePerPatch, value);
-        }
-
         public static Operand Cbuf(int slot, int offset)
         {
             return new Operand(slot, offset);
diff --git a/Ryujinx.Graphics.Shader/IntermediateRepresentation/OperandType.cs b/Ryujinx.Graphics.Shader/IntermediateRepresentation/OperandType.cs
index 7566a03f..4d2da734 100644
--- a/Ryujinx.Graphics.Shader/IntermediateRepresentation/OperandType.cs
+++ b/Ryujinx.Graphics.Shader/IntermediateRepresentation/OperandType.cs
@@ -3,8 +3,6 @@ namespace Ryujinx.Graphics.Shader.IntermediateRepresentation
     enum OperandType
     {
         Argument,
-        Attribute,
-        AttributePerPatch,
         Constant,
         ConstantBuffer,
         Label,
@@ -12,12 +10,4 @@ namespace Ryujinx.Graphics.Shader.IntermediateRepresentation
         Register,
         Undefined
     }
-
-    static class OperandTypeExtensions
-    {
-        public static bool IsAttribute(this OperandType type)
-        {
-            return type == OperandType.Attribute || type == OperandType.AttributePerPatch;
-        }
-    }
 }
\ No newline at end of file
diff --git a/Ryujinx.Graphics.Shader/IntermediateRepresentation/Operation.cs b/Ryujinx.Graphics.Shader/IntermediateRepresentation/Operation.cs
index 18e203a7..99179f15 100644
--- a/Ryujinx.Graphics.Shader/IntermediateRepresentation/Operation.cs
+++ b/Ryujinx.Graphics.Shader/IntermediateRepresentation/Operation.cs
@@ -6,6 +6,7 @@ namespace Ryujinx.Graphics.Shader.IntermediateRepresentation
     class Operation : INode
     {
         public Instruction Inst { get; private set; }
+        public StorageKind StorageKind { get; }
 
         private Operand[] _dests;
 
@@ -99,6 +100,23 @@ namespace Ryujinx.Graphics.Shader.IntermediateRepresentation
             }
         }
 
+        public Operation(Instruction inst, StorageKind storageKind, Operand dest, params Operand[] sources) : this(sources)
+        {
+            Inst = inst;
+            StorageKind = storageKind;
+
+            if (dest != null)
+            {
+                dest.AsgOp = this;
+
+                _dests = new[] { dest };
+            }
+            else
+            {
+                _dests = Array.Empty<Operand>();
+            }
+        }
+
         public Operation(Instruction inst, int index, Operand dest, params Operand[] sources) : this(inst, dest, sources)
         {
             Index = index;
diff --git a/Ryujinx.Graphics.Shader/IntermediateRepresentation/StorageKind.cs b/Ryujinx.Graphics.Shader/IntermediateRepresentation/StorageKind.cs
new file mode 100644
index 00000000..59357443
--- /dev/null
+++ b/Ryujinx.Graphics.Shader/IntermediateRepresentation/StorageKind.cs
@@ -0,0 +1,39 @@
+namespace Ryujinx.Graphics.Shader.IntermediateRepresentation
+{
+    enum StorageKind
+    {
+        None,
+        Input,
+        InputPerPatch,
+        Output,
+        OutputPerPatch,
+        ConstantBuffer,
+        StorageBuffer,
+        LocalMemory,
+        SharedMemory,
+        GlobalMemory
+    }
+
+    static class StorageKindExtensions
+    {
+        public static bool IsInputOrOutput(this StorageKind storageKind)
+        {
+            return storageKind == StorageKind.Input ||
+                   storageKind == StorageKind.InputPerPatch ||
+                   storageKind == StorageKind.Output ||
+                   storageKind == StorageKind.OutputPerPatch;
+        }
+
+        public static bool IsOutput(this StorageKind storageKind)
+        {
+            return storageKind == StorageKind.Output ||
+                   storageKind == StorageKind.OutputPerPatch;
+        }
+
+        public static bool IsPerPatch(this StorageKind storageKind)
+        {
+            return storageKind == StorageKind.InputPerPatch ||
+                   storageKind == StorageKind.OutputPerPatch;
+        }
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.Graphics.Shader/StructuredIr/AstOperation.cs b/Ryujinx.Graphics.Shader/StructuredIr/AstOperation.cs
index 19397256..2393fd8d 100644
--- a/Ryujinx.Graphics.Shader/StructuredIr/AstOperation.cs
+++ b/Ryujinx.Graphics.Shader/StructuredIr/AstOperation.cs
@@ -9,6 +9,7 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
     class AstOperation : AstNode
     {
         public Instruction Inst { get; }
+        public StorageKind StorageKind { get; }
 
         public int Index { get; }
 
@@ -16,9 +17,10 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
 
         public int SourcesCount => _sources.Length;
 
-        public AstOperation(Instruction inst, IAstNode[] sources, int sourcesCount)
+        public AstOperation(Instruction inst, StorageKind storageKind, IAstNode[] sources, int sourcesCount)
         {
-            Inst     = inst;
+            Inst = inst;
+            StorageKind = storageKind;
             _sources = sources;
 
             for (int index = 0; index < sources.Length; index++)
@@ -36,12 +38,12 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
             Index = 0;
         }
 
-        public AstOperation(Instruction inst, int index, IAstNode[] sources, int sourcesCount) : this(inst, sources, sourcesCount)
+        public AstOperation(Instruction inst, StorageKind storageKind, int index, IAstNode[] sources, int sourcesCount) : this(inst, storageKind, sources, sourcesCount)
         {
             Index = index;
         }
 
-        public AstOperation(Instruction inst, params IAstNode[] sources) : this(inst, sources, sources.Length)
+        public AstOperation(Instruction inst, params IAstNode[] sources) : this(inst, StorageKind.None, sources, sources.Length)
         {
         }
 
diff --git a/Ryujinx.Graphics.Shader/StructuredIr/AstTextureOperation.cs b/Ryujinx.Graphics.Shader/StructuredIr/AstTextureOperation.cs
index 957a956f..a44f13cc 100644
--- a/Ryujinx.Graphics.Shader/StructuredIr/AstTextureOperation.cs
+++ b/Ryujinx.Graphics.Shader/StructuredIr/AstTextureOperation.cs
@@ -19,7 +19,7 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
             int cbufSlot,
             int handle,
             int index,
-            params IAstNode[] sources) : base(inst, index, sources, sources.Length)
+            params IAstNode[] sources) : base(inst, StorageKind.None, index, sources, sources.Length)
         {
             Type = type;
             Format = format;
diff --git a/Ryujinx.Graphics.Shader/StructuredIr/InstructionInfo.cs b/Ryujinx.Graphics.Shader/StructuredIr/InstructionInfo.cs
index 0a9a9e51..8eccef23 100644
--- a/Ryujinx.Graphics.Shader/StructuredIr/InstructionInfo.cs
+++ b/Ryujinx.Graphics.Shader/StructuredIr/InstructionInfo.cs
@@ -89,7 +89,7 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
             Add(Instruction.ImageStore,               AggregateType.Void);
             Add(Instruction.ImageAtomic,              AggregateType.S32);
             Add(Instruction.IsNan,                    AggregateType.Bool,   AggregateType.Scalar);
-            Add(Instruction.LoadAttribute,            AggregateType.FP32,   AggregateType.S32,     AggregateType.S32,     AggregateType.S32);
+            Add(Instruction.Load,                     AggregateType.FP32);
             Add(Instruction.LoadConstant,             AggregateType.FP32,   AggregateType.S32,     AggregateType.S32);
             Add(Instruction.LoadGlobal,               AggregateType.U32,    AggregateType.S32,     AggregateType.S32);
             Add(Instruction.LoadLocal,                AggregateType.U32,    AggregateType.S32);
@@ -122,7 +122,7 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
             Add(Instruction.ShuffleXor,               AggregateType.FP32,   AggregateType.FP32,    AggregateType.U32,     AggregateType.U32,     AggregateType.Bool);
             Add(Instruction.Sine,                     AggregateType.Scalar, AggregateType.Scalar);
             Add(Instruction.SquareRoot,               AggregateType.Scalar, AggregateType.Scalar);
-            Add(Instruction.StoreAttribute,           AggregateType.Void,   AggregateType.S32,     AggregateType.S32,     AggregateType.FP32);
+            Add(Instruction.Store,                    AggregateType.Void);
             Add(Instruction.StoreGlobal,              AggregateType.Void,   AggregateType.S32,     AggregateType.S32,     AggregateType.U32);
             Add(Instruction.StoreLocal,               AggregateType.Void,   AggregateType.S32,     AggregateType.U32);
             Add(Instruction.StoreShared,              AggregateType.Void,   AggregateType.S32,     AggregateType.U32);
@@ -166,7 +166,7 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
             {
                 return AggregateType.FP32;
             }
-            else if (inst == Instruction.Call)
+            else if (inst == Instruction.Call || inst == Instruction.Load || inst == Instruction.Store)
             {
                 return AggregateType.S32;
             }
diff --git a/Ryujinx.Graphics.Shader/StructuredIr/IoDefinition.cs b/Ryujinx.Graphics.Shader/StructuredIr/IoDefinition.cs
new file mode 100644
index 00000000..21a1b3f0
--- /dev/null
+++ b/Ryujinx.Graphics.Shader/StructuredIr/IoDefinition.cs
@@ -0,0 +1,44 @@
+using Ryujinx.Graphics.Shader.IntermediateRepresentation;
+using System;
+
+namespace Ryujinx.Graphics.Shader.StructuredIr
+{
+    readonly struct IoDefinition : IEquatable<IoDefinition>
+    {
+        public StorageKind StorageKind { get; }
+        public IoVariable IoVariable { get; }
+        public int Location { get; }
+        public int Component { get; }
+
+        public IoDefinition(StorageKind storageKind, IoVariable ioVariable, int location = 0, int component = 0)
+        {
+            StorageKind = storageKind;
+            IoVariable = ioVariable;
+            Location = location;
+            Component = component;
+        }
+
+        public override bool Equals(object other)
+        {
+            return other is IoDefinition ioDefinition && Equals(ioDefinition);
+        }
+
+        public bool Equals(IoDefinition other)
+        {
+            return StorageKind == other.StorageKind &&
+                   IoVariable == other.IoVariable &&
+                   Location == other.Location &&
+                   Component == other.Component;
+        }
+
+        public override int GetHashCode()
+        {
+            return (int)StorageKind | ((int)IoVariable << 8) | (Location << 16) | (Component << 24);
+        }
+
+        public override string ToString()
+        {
+            return $"{StorageKind}.{IoVariable}.{Location}.{Component}";
+        }
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.Graphics.Shader/StructuredIr/OperandInfo.cs b/Ryujinx.Graphics.Shader/StructuredIr/OperandInfo.cs
index 730468a4..38ed1584 100644
--- a/Ryujinx.Graphics.Shader/StructuredIr/OperandInfo.cs
+++ b/Ryujinx.Graphics.Shader/StructuredIr/OperandInfo.cs
@@ -23,8 +23,6 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
             return type switch
             {
                 OperandType.Argument => AggregateType.S32,
-                OperandType.Attribute => AggregateType.FP32,
-                OperandType.AttributePerPatch => AggregateType.FP32,
                 OperandType.Constant => AggregateType.S32,
                 OperandType.ConstantBuffer => AggregateType.FP32,
                 OperandType.Undefined => AggregateType.S32,
diff --git a/Ryujinx.Graphics.Shader/StructuredIr/StructuredProgram.cs b/Ryujinx.Graphics.Shader/StructuredIr/StructuredProgram.cs
index b8d38fa6..b4ca8ee5 100644
--- a/Ryujinx.Graphics.Shader/StructuredIr/StructuredProgram.cs
+++ b/Ryujinx.Graphics.Shader/StructuredIr/StructuredProgram.cs
@@ -65,49 +65,35 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
                 context.LeaveFunction();
             }
 
-            if (config.TransformFeedbackEnabled && (config.LastInVertexPipeline || config.Stage == ShaderStage.Fragment))
-            {
-                for (int tfbIndex = 0; tfbIndex < 4; tfbIndex++)
-                {
-                    var locations = config.GpuAccessor.QueryTransformFeedbackVaryingLocations(tfbIndex);
-                    var stride = config.GpuAccessor.QueryTransformFeedbackStride(tfbIndex);
-
-                    for (int i = 0; i < locations.Length; i++)
-                    {
-                        byte location = locations[i];
-                        if (location < 0xc0)
-                        {
-                            context.Info.TransformFeedbackOutputs[location] = new TransformFeedbackOutput(tfbIndex, i * 4, stride);
-                        }
-                    }
-                }
-            }
-
             return context.Info;
         }
 
         private static void AddOperation(StructuredProgramContext context, Operation operation)
         {
             Instruction inst = operation.Inst;
+            StorageKind storageKind = operation.StorageKind;
 
-            if (inst == Instruction.LoadAttribute)
+            if ((inst == Instruction.Load || inst == Instruction.Store) && storageKind.IsInputOrOutput())
             {
-                Operand src1 = operation.GetSource(0);
-                Operand src2 = operation.GetSource(1);
+                IoVariable ioVariable = (IoVariable)operation.GetSource(0).Value;
+                bool isOutput = storageKind.IsOutput();
+                bool perPatch = storageKind.IsPerPatch();
+                int location = 0;
+                int component = 0;
 
-                if (src1.Type == OperandType.Constant && src2.Type == OperandType.Constant)
+                if (context.Config.HasPerLocationInputOrOutput(ioVariable, isOutput))
                 {
-                    int attrOffset = (src1.Value & AttributeConsts.Mask) + (src2.Value << 2);
+                    location = operation.GetSource(1).Value;
 
-                    if ((src1.Value & AttributeConsts.LoadOutputMask) != 0)
+                    if (operation.SourcesCount > 2 &&
+                        operation.GetSource(2).Type == OperandType.Constant &&
+                        context.Config.HasPerLocationInputOrOutputComponent(ioVariable, location, operation.GetSource(2).Value, isOutput))
                     {
-                        context.Info.Outputs.Add(attrOffset);
-                    }
-                    else
-                    {
-                        context.Info.Inputs.Add(attrOffset);
+                        component = operation.GetSource(2).Value;
                     }
                 }
+
+                context.Info.IoDefinitions.Add(new IoDefinition(storageKind, ioVariable, location, component));
             }
 
             bool vectorDest = IsVectorDestInst(inst);
@@ -119,12 +105,12 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
 
             for (int index = 0; index < operation.SourcesCount; index++)
             {
-                sources[index] = context.GetOperandUse(operation.GetSource(index));
+                sources[index] = context.GetOperand(operation.GetSource(index));
             }
 
             for (int index = 0; index < outDestsCount; index++)
             {
-                AstOperand oper = context.GetOperandDef(operation.GetDest(1 + index));
+                AstOperand oper = context.GetOperand(operation.GetDest(1 + index));
 
                 oper.VarType = InstructionInfo.GetSrcVarType(inst, sourcesCount + index);
 
@@ -163,7 +149,7 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
                 }
                 else
                 {
-                    source = new AstOperation(inst, operation.Index, sources, operation.SourcesCount);
+                    source = new AstOperation(inst, operation.StorageKind, operation.Index, sources, operation.SourcesCount);
                 }
 
                 AggregateType destElemType = destType;
@@ -181,17 +167,17 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
 
                 for (int i = 0; i < operation.DestsCount; i++)
                 {
-                    AstOperand dest = context.GetOperandDef(operation.GetDest(i));
+                    AstOperand dest = context.GetOperand(operation.GetDest(i));
                     AstOperand index = new AstOperand(OperandType.Constant, i);
 
                     dest.VarType = destElemType;
 
-                    context.AddNode(new AstAssignment(dest, new AstOperation(Instruction.VectorExtract, new[] { destVec, index }, 2)));
+                    context.AddNode(new AstAssignment(dest, new AstOperation(Instruction.VectorExtract, StorageKind.None, new[] { destVec, index }, 2)));
                 }
             }
             else if (operation.Dest != null)
             {
-                AstOperand dest = context.GetOperandDef(operation.Dest);
+                AstOperand dest = context.GetOperand(operation.Dest);
 
                 // If all the sources are bool, it's better to use short-circuiting
                 // logical operations, rather than forcing a cast to int and doing
@@ -234,7 +220,7 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
                 }
                 else if (!isCopy)
                 {
-                    source = new AstOperation(inst, operation.Index, sources, operation.SourcesCount);
+                    source = new AstOperation(inst, operation.StorageKind, operation.Index, sources, operation.SourcesCount);
                 }
                 else
                 {
@@ -255,7 +241,7 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
             }
             else
             {
-                context.AddNode(new AstOperation(inst, operation.Index, sources, operation.SourcesCount));
+                context.AddNode(new AstOperation(inst, operation.StorageKind, operation.Index, sources, operation.SourcesCount));
             }
 
             // Those instructions needs to be emulated by using helper functions,
@@ -263,13 +249,16 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
             // decide which helper functions are needed on the final generated code.
             switch (operation.Inst)
             {
-                case Instruction.AtomicMaxS32 | Instruction.MrShared:
-                case Instruction.AtomicMinS32 | Instruction.MrShared:
-                    context.Info.HelperFunctionsMask |= HelperFunctionsMask.AtomicMinMaxS32Shared;
-                    break;
-                case Instruction.AtomicMaxS32 | Instruction.MrStorage:
-                case Instruction.AtomicMinS32 | Instruction.MrStorage:
-                    context.Info.HelperFunctionsMask |= HelperFunctionsMask.AtomicMinMaxS32Storage;
+                case Instruction.AtomicMaxS32:
+                case Instruction.AtomicMinS32:
+                    if (operation.StorageKind == StorageKind.SharedMemory)
+                    {
+                        context.Info.HelperFunctionsMask |= HelperFunctionsMask.AtomicMinMaxS32Shared;
+                    }
+                    else if (operation.StorageKind == StorageKind.StorageBuffer)
+                    {
+                        context.Info.HelperFunctionsMask |= HelperFunctionsMask.AtomicMinMaxS32Storage;
+                    }
                     break;
                 case Instruction.MultiplyHighS32:
                     context.Info.HelperFunctionsMask |= HelperFunctionsMask.MultiplyHighS32;
diff --git a/Ryujinx.Graphics.Shader/StructuredIr/StructuredProgramContext.cs b/Ryujinx.Graphics.Shader/StructuredIr/StructuredProgramContext.cs
index ce57a578..68bbdeb1 100644
--- a/Ryujinx.Graphics.Shader/StructuredIr/StructuredProgramContext.cs
+++ b/Ryujinx.Graphics.Shader/StructuredIr/StructuredProgramContext.cs
@@ -37,43 +37,26 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
 
             Config = config;
 
-            if (config.Stage == ShaderStage.TessellationControl)
-            {
-                // Required to index outputs.
-                Info.Inputs.Add(AttributeConsts.InvocationId);
-            }
-            else if (config.GpPassthrough)
+            if (config.GpPassthrough)
             {
                 int passthroughAttributes = config.PassthroughAttributes;
                 while (passthroughAttributes != 0)
                 {
                     int index = BitOperations.TrailingZeroCount(passthroughAttributes);
 
-                    int attrBase = AttributeConsts.UserAttributeBase + index * 16;
-                    Info.Inputs.Add(attrBase);
-                    Info.Inputs.Add(attrBase + 4);
-                    Info.Inputs.Add(attrBase + 8);
-                    Info.Inputs.Add(attrBase + 12);
+                    Info.IoDefinitions.Add(new IoDefinition(StorageKind.Input, IoVariable.UserDefined, index));
 
                     passthroughAttributes &= ~(1 << index);
                 }
 
-                Info.Inputs.Add(AttributeConsts.PositionX);
-                Info.Inputs.Add(AttributeConsts.PositionY);
-                Info.Inputs.Add(AttributeConsts.PositionZ);
-                Info.Inputs.Add(AttributeConsts.PositionW);
-                Info.Inputs.Add(AttributeConsts.PointSize);
-
-                for (int i = 0; i < 8; i++)
-                {
-                    Info.Inputs.Add(AttributeConsts.ClipDistance0 + i * 4);
-                }
+                Info.IoDefinitions.Add(new IoDefinition(StorageKind.Input, IoVariable.Position));
+                Info.IoDefinitions.Add(new IoDefinition(StorageKind.Input, IoVariable.PointSize));
+                Info.IoDefinitions.Add(new IoDefinition(StorageKind.Input, IoVariable.ClipDistance));
             }
             else if (config.Stage == ShaderStage.Fragment)
             {
                 // Potentially used for texture coordinate scaling.
-                Info.Inputs.Add(AttributeConsts.PositionX);
-                Info.Inputs.Add(AttributeConsts.PositionY);
+                Info.IoDefinitions.Add(new IoDefinition(StorageKind.Input, IoVariable.FragmentCoord));
             }
         }
 
@@ -281,7 +264,7 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
             }
             else
             {
-                cond = GetOperandUse(branchOp.GetSource(0));
+                cond = GetOperand(branchOp.GetSource(0));
 
                 Instruction invInst = type == AstBlockType.If
                     ? Instruction.BranchIfTrue
@@ -315,41 +298,7 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
             return newTemp;
         }
 
-        public AstOperand GetOperandDef(Operand operand)
-        {
-            if (operand.Type == OperandType.Attribute)
-            {
-                Info.Outputs.Add(operand.Value & AttributeConsts.Mask);
-            }
-            else if (operand.Type == OperandType.AttributePerPatch)
-            {
-                Info.OutputsPerPatch.Add(operand.Value & AttributeConsts.Mask);
-            }
-
-            return GetOperand(operand);
-        }
-
-        public AstOperand GetOperandUse(Operand operand)
-        {
-            // If this flag is set, we're reading from an output attribute instead.
-            if (operand.Type.IsAttribute() && (operand.Value & AttributeConsts.LoadOutputMask) != 0)
-            {
-                return GetOperandDef(operand);
-            }
-
-            if (operand.Type == OperandType.Attribute)
-            {
-                Info.Inputs.Add(operand.Value);
-            }
-            else if (operand.Type == OperandType.AttributePerPatch)
-            {
-                Info.InputsPerPatch.Add(operand.Value);
-            }
-
-            return GetOperand(operand);
-        }
-
-        private AstOperand GetOperand(Operand operand)
+        public AstOperand GetOperand(Operand operand)
         {
             if (operand == null)
             {
diff --git a/Ryujinx.Graphics.Shader/StructuredIr/StructuredProgramInfo.cs b/Ryujinx.Graphics.Shader/StructuredIr/StructuredProgramInfo.cs
index 489a5910..c5104146 100644
--- a/Ryujinx.Graphics.Shader/StructuredIr/StructuredProgramInfo.cs
+++ b/Ryujinx.Graphics.Shader/StructuredIr/StructuredProgramInfo.cs
@@ -22,60 +22,15 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
     {
         public List<StructuredFunction> Functions { get; }
 
-        public HashSet<int> Inputs { get; }
-        public HashSet<int> Outputs { get; }
-        public HashSet<int> InputsPerPatch { get; }
-        public HashSet<int> OutputsPerPatch { get; }
+        public HashSet<IoDefinition> IoDefinitions { get; }
 
         public HelperFunctionsMask HelperFunctionsMask { get; set; }
 
-        public TransformFeedbackOutput[] TransformFeedbackOutputs { get; }
-
         public StructuredProgramInfo()
         {
             Functions = new List<StructuredFunction>();
 
-            Inputs = new HashSet<int>();
-            Outputs = new HashSet<int>();
-            InputsPerPatch = new HashSet<int>();
-            OutputsPerPatch = new HashSet<int>();
-
-            TransformFeedbackOutputs = new TransformFeedbackOutput[0xc0];
-        }
-
-        public TransformFeedbackOutput GetTransformFeedbackOutput(int attr)
-        {
-            int index = attr / 4;
-            return TransformFeedbackOutputs[index];
-        }
-
-        public int GetTransformFeedbackOutputComponents(int attr)
-        {
-            int index = attr / 4;
-            int baseIndex = index & ~3;
-
-            int count = 1;
-
-            for (; count < 4; count++)
-            {
-                ref var prev = ref TransformFeedbackOutputs[baseIndex + count - 1];
-                ref var curr = ref TransformFeedbackOutputs[baseIndex + count];
-
-                int prevOffset = prev.Offset;
-                int currOffset = curr.Offset;
-
-                if (!prev.Valid || !curr.Valid || prevOffset + 4 != currOffset)
-                {
-                    break;
-                }
-            }
-
-            if (baseIndex + count <= index)
-            {
-                return 1;
-            }
-
-            return count;
+            IoDefinitions = new HashSet<IoDefinition>();
         }
     }
 }
\ No newline at end of file
diff --git a/Ryujinx.Graphics.Shader/Translation/AttributeConsts.cs b/Ryujinx.Graphics.Shader/Translation/AttributeConsts.cs
index 08efbc9f..683b0d8a 100644
--- a/Ryujinx.Graphics.Shader/Translation/AttributeConsts.cs
+++ b/Ryujinx.Graphics.Shader/Translation/AttributeConsts.cs
@@ -2,104 +2,35 @@ namespace Ryujinx.Graphics.Shader.Translation
 {
     static class AttributeConsts
     {
-        public const int TessLevelOuter0     = 0x000;
-        public const int TessLevelOuter1     = 0x004;
-        public const int TessLevelOuter2     = 0x008;
-        public const int TessLevelOuter3     = 0x00c;
-        public const int TessLevelInner0     = 0x010;
-        public const int TessLevelInner1     = 0x014;
-        public const int PrimitiveId         = 0x060;
-        public const int Layer               = 0x064;
-        public const int ViewportIndex       = 0x068;
-        public const int PointSize           = 0x06c;
-        public const int PositionX           = 0x070;
-        public const int PositionY           = 0x074;
-        public const int PositionZ           = 0x078;
-        public const int PositionW           = 0x07c;
-        public const int FrontColorDiffuseR  = 0x280;
-        public const int FrontColorDiffuseG  = 0x284;
-        public const int FrontColorDiffuseB  = 0x288;
-        public const int FrontColorDiffuseA  = 0x28c;
-        public const int FrontColorSpecularR = 0x290;
-        public const int FrontColorSpecularG = 0x294;
-        public const int FrontColorSpecularB = 0x298;
-        public const int FrontColorSpecularA = 0x29c;
-        public const int BackColorDiffuseR   = 0x2a0;
-        public const int BackColorDiffuseG   = 0x2a4;
-        public const int BackColorDiffuseB   = 0x2a8;
-        public const int BackColorDiffuseA   = 0x2ac;
-        public const int BackColorSpecularR  = 0x2b0;
-        public const int BackColorSpecularG  = 0x2b4;
-        public const int BackColorSpecularB  = 0x2b8;
-        public const int BackColorSpecularA  = 0x2bc;
-        public const int ClipDistance0       = 0x2c0;
-        public const int ClipDistance1       = 0x2c4;
-        public const int ClipDistance2       = 0x2c8;
-        public const int ClipDistance3       = 0x2cc;
-        public const int ClipDistance4       = 0x2d0;
-        public const int ClipDistance5       = 0x2d4;
-        public const int ClipDistance6       = 0x2d8;
-        public const int ClipDistance7       = 0x2dc;
-        public const int PointCoordX         = 0x2e0;
-        public const int PointCoordY         = 0x2e4;
-        public const int TessCoordX          = 0x2f0;
-        public const int TessCoordY          = 0x2f4;
-        public const int InstanceId          = 0x2f8;
-        public const int VertexId            = 0x2fc;
-        public const int TexCoordCount       = 10;
-        public const int TexCoordBase        = 0x300;
-        public const int TexCoordEnd         = TexCoordBase + TexCoordCount * 16;
-        public const int FrontFacing         = 0x3fc;
+        public const int PrimitiveId = 0x060;
+        public const int Layer = 0x064;
+        public const int PositionX = 0x070;
+        public const int PositionY = 0x074;
+        public const int FrontColorDiffuseR = 0x280;
+        public const int BackColorDiffuseR = 0x2a0;
+        public const int ClipDistance0 = 0x2c0;
+        public const int ClipDistance1 = 0x2c4;
+        public const int ClipDistance2 = 0x2c8;
+        public const int ClipDistance3 = 0x2cc;
+        public const int ClipDistance4 = 0x2d0;
+        public const int ClipDistance5 = 0x2d4;
+        public const int ClipDistance6 = 0x2d8;
+        public const int ClipDistance7 = 0x2dc;
+        public const int FogCoord = 0x2e8;
+        public const int TessCoordX = 0x2f0;
+        public const int TessCoordY = 0x2f4;
+        public const int InstanceId = 0x2f8;
+        public const int VertexId = 0x2fc;
+        public const int TexCoordCount = 10;
+        public const int TexCoordBase = 0x300;
+        public const int TexCoordEnd = TexCoordBase + TexCoordCount * 16;
+        public const int FrontFacing = 0x3fc;
 
         public const int UserAttributesCount = 32;
-        public const int UserAttributeBase   = 0x80;
-        public const int UserAttributeEnd    = UserAttributeBase + UserAttributesCount * 16;
+        public const int UserAttributeBase = 0x80;
+        public const int UserAttributeEnd = UserAttributeBase + UserAttributesCount * 16;
 
         public const int UserAttributePerPatchBase = 0x18;
-        public const int UserAttributePerPatchEnd  = 0x200;
-
-        public const int LoadOutputMask = 1 << 30;
-        public const int Mask = 0x3fffffff;
-
-
-        // Note: Those attributes are used internally by the translator
-        // only, they don't exist on Maxwell.
-        public const int SpecialMask             = 0xf << 24;
-        public const int FragmentOutputDepth     = 0x1000000;
-        public const int FragmentOutputColorBase = 0x1000010;
-        public const int FragmentOutputColorEnd  = FragmentOutputColorBase + 8 * 16;
-
-        public const int FragmentOutputIsBgraBase = 0x1000100;
-        public const int FragmentOutputIsBgraEnd  = FragmentOutputIsBgraBase + 8 * 4;
-
-        public const int SupportBlockViewInverseX = 0x1000200;
-        public const int SupportBlockViewInverseY = 0x1000204;
-
-        public const int ThreadIdX = 0x2000000;
-        public const int ThreadIdY = 0x2000004;
-        public const int ThreadIdZ = 0x2000008;
-
-        public const int CtaIdX = 0x2000010;
-        public const int CtaIdY = 0x2000014;
-        public const int CtaIdZ = 0x2000018;
-
-        public const int LaneId = 0x2000020;
-
-        public const int InvocationId = 0x2000024;
-        public const int PatchVerticesIn = 0x2000028;
-
-        public const int EqMask = 0x2000030;
-        public const int GeMask = 0x2000034;
-        public const int GtMask = 0x2000038;
-        public const int LeMask = 0x200003c;
-        public const int LtMask = 0x2000040;
-
-        public const int ThreadKill = 0x2000044;
-
-        public const int BaseInstance = 0x2000050;
-        public const int BaseVertex = 0x2000054;
-        public const int InstanceIndex = 0x2000058;
-        public const int VertexIndex = 0x200005c;
-        public const int DrawIndex = 0x2000060;
+        public const int UserAttributePerPatchEnd = 0x200;
     }
 }
\ No newline at end of file
diff --git a/Ryujinx.Graphics.Shader/Translation/AttributeInfo.cs b/Ryujinx.Graphics.Shader/Translation/AttributeInfo.cs
deleted file mode 100644
index b671429a..00000000
--- a/Ryujinx.Graphics.Shader/Translation/AttributeInfo.cs
+++ /dev/null
@@ -1,210 +0,0 @@
-using System.Collections.Generic;
-
-namespace Ryujinx.Graphics.Shader.Translation
-{
-    readonly struct AttributeInfo
-    {
-        private static readonly Dictionary<int, AttributeInfo> _builtInAttributes = new Dictionary<int, AttributeInfo>()
-        {
-            { AttributeConsts.Layer,         new AttributeInfo(AttributeConsts.Layer,         0, 1, AggregateType.S32) },
-            { AttributeConsts.ViewportIndex, new AttributeInfo(AttributeConsts.ViewportIndex, 0, 1, AggregateType.S32) },
-            { AttributeConsts.PointSize,     new AttributeInfo(AttributeConsts.PointSize,     0, 1, AggregateType.FP32) },
-            { AttributeConsts.PositionX,     new AttributeInfo(AttributeConsts.PositionX,     0, 4, AggregateType.Vector4 | AggregateType.FP32) },
-            { AttributeConsts.PositionY,     new AttributeInfo(AttributeConsts.PositionX,     1, 4, AggregateType.Vector4 | AggregateType.FP32) },
-            { AttributeConsts.PositionZ,     new AttributeInfo(AttributeConsts.PositionX,     2, 4, AggregateType.Vector4 | AggregateType.FP32) },
-            { AttributeConsts.PositionW,     new AttributeInfo(AttributeConsts.PositionX,     3, 4, AggregateType.Vector4 | AggregateType.FP32) },
-            { AttributeConsts.ClipDistance0, new AttributeInfo(AttributeConsts.ClipDistance0, 0, 8, AggregateType.Array   | AggregateType.FP32) },
-            { AttributeConsts.ClipDistance1, new AttributeInfo(AttributeConsts.ClipDistance0, 1, 8, AggregateType.Array   | AggregateType.FP32) },
-            { AttributeConsts.ClipDistance2, new AttributeInfo(AttributeConsts.ClipDistance0, 2, 8, AggregateType.Array   | AggregateType.FP32) },
-            { AttributeConsts.ClipDistance3, new AttributeInfo(AttributeConsts.ClipDistance0, 3, 8, AggregateType.Array   | AggregateType.FP32) },
-            { AttributeConsts.ClipDistance4, new AttributeInfo(AttributeConsts.ClipDistance0, 4, 8, AggregateType.Array   | AggregateType.FP32) },
-            { AttributeConsts.ClipDistance5, new AttributeInfo(AttributeConsts.ClipDistance0, 5, 8, AggregateType.Array   | AggregateType.FP32) },
-            { AttributeConsts.ClipDistance6, new AttributeInfo(AttributeConsts.ClipDistance0, 6, 8, AggregateType.Array   | AggregateType.FP32) },
-            { AttributeConsts.ClipDistance7, new AttributeInfo(AttributeConsts.ClipDistance0, 7, 8, AggregateType.Array   | AggregateType.FP32) },
-            { AttributeConsts.PointCoordX,   new AttributeInfo(AttributeConsts.PointCoordX,   0, 2, AggregateType.Vector4 | AggregateType.FP32) },
-            { AttributeConsts.PointCoordY,   new AttributeInfo(AttributeConsts.PointCoordX,   1, 2, AggregateType.Vector4 | AggregateType.FP32) },
-            { AttributeConsts.TessCoordX,    new AttributeInfo(AttributeConsts.TessCoordX,    0, 3, AggregateType.Vector4 | AggregateType.FP32) },
-            { AttributeConsts.TessCoordY,    new AttributeInfo(AttributeConsts.TessCoordX,    1, 3, AggregateType.Vector4 | AggregateType.FP32) },
-            { AttributeConsts.InstanceId,    new AttributeInfo(AttributeConsts.InstanceId,    0, 1, AggregateType.S32) },
-            { AttributeConsts.VertexId,      new AttributeInfo(AttributeConsts.VertexId,      0, 1, AggregateType.S32) },
-            { AttributeConsts.BaseInstance,  new AttributeInfo(AttributeConsts.BaseInstance,  0, 1, AggregateType.S32) },
-            { AttributeConsts.BaseVertex,    new AttributeInfo(AttributeConsts.BaseVertex,    0, 1, AggregateType.S32) },
-            { AttributeConsts.InstanceIndex, new AttributeInfo(AttributeConsts.InstanceIndex, 0, 1, AggregateType.S32) },
-            { AttributeConsts.VertexIndex,   new AttributeInfo(AttributeConsts.VertexIndex,   0, 1, AggregateType.S32) },
-            { AttributeConsts.DrawIndex,     new AttributeInfo(AttributeConsts.DrawIndex,     0, 1, AggregateType.S32) },
-            { AttributeConsts.FrontFacing,   new AttributeInfo(AttributeConsts.FrontFacing,   0, 1, AggregateType.Bool) },
-
-            // Special.
-            { AttributeConsts.FragmentOutputDepth, new AttributeInfo(AttributeConsts.FragmentOutputDepth, 0, 1, AggregateType.FP32) },
-            { AttributeConsts.ThreadKill,          new AttributeInfo(AttributeConsts.ThreadKill,          0, 1, AggregateType.Bool) },
-            { AttributeConsts.ThreadIdX,           new AttributeInfo(AttributeConsts.ThreadIdX,           0, 3, AggregateType.Vector3 | AggregateType.U32) },
-            { AttributeConsts.ThreadIdY,           new AttributeInfo(AttributeConsts.ThreadIdX,           1, 3, AggregateType.Vector3 | AggregateType.U32) },
-            { AttributeConsts.ThreadIdZ,           new AttributeInfo(AttributeConsts.ThreadIdX,           2, 3, AggregateType.Vector3 | AggregateType.U32) },
-            { AttributeConsts.CtaIdX,              new AttributeInfo(AttributeConsts.CtaIdX,              0, 3, AggregateType.Vector3 | AggregateType.U32) },
-            { AttributeConsts.CtaIdY,              new AttributeInfo(AttributeConsts.CtaIdX,              1, 3, AggregateType.Vector3 | AggregateType.U32) },
-            { AttributeConsts.CtaIdZ,              new AttributeInfo(AttributeConsts.CtaIdX,              2, 3, AggregateType.Vector3 | AggregateType.U32) },
-            { AttributeConsts.LaneId,              new AttributeInfo(AttributeConsts.LaneId,              0, 1, AggregateType.U32) },
-            { AttributeConsts.InvocationId,        new AttributeInfo(AttributeConsts.InvocationId,        0, 1, AggregateType.S32) },
-            { AttributeConsts.PrimitiveId,         new AttributeInfo(AttributeConsts.PrimitiveId,         0, 1, AggregateType.S32) },
-            { AttributeConsts.PatchVerticesIn,     new AttributeInfo(AttributeConsts.PatchVerticesIn,     0, 1, AggregateType.S32) },
-            { AttributeConsts.EqMask,              new AttributeInfo(AttributeConsts.EqMask,              0, 4, AggregateType.Vector4 | AggregateType.U32) },
-            { AttributeConsts.GeMask,              new AttributeInfo(AttributeConsts.GeMask,              0, 4, AggregateType.Vector4 | AggregateType.U32) },
-            { AttributeConsts.GtMask,              new AttributeInfo(AttributeConsts.GtMask,              0, 4, AggregateType.Vector4 | AggregateType.U32) },
-            { AttributeConsts.LeMask,              new AttributeInfo(AttributeConsts.LeMask,              0, 4, AggregateType.Vector4 | AggregateType.U32) },
-            { AttributeConsts.LtMask,              new AttributeInfo(AttributeConsts.LtMask,              0, 4, AggregateType.Vector4 | AggregateType.U32) },
-        };
-
-        private static readonly Dictionary<int, AttributeInfo> _builtInAttributesPerPatch = new Dictionary<int, AttributeInfo>()
-        {
-            { AttributeConsts.TessLevelOuter0, new AttributeInfo(AttributeConsts.TessLevelOuter0, 0, 4, AggregateType.Array | AggregateType.FP32) },
-            { AttributeConsts.TessLevelOuter1, new AttributeInfo(AttributeConsts.TessLevelOuter0, 1, 4, AggregateType.Array | AggregateType.FP32) },
-            { AttributeConsts.TessLevelOuter2, new AttributeInfo(AttributeConsts.TessLevelOuter0, 2, 4, AggregateType.Array | AggregateType.FP32) },
-            { AttributeConsts.TessLevelOuter3, new AttributeInfo(AttributeConsts.TessLevelOuter0, 3, 4, AggregateType.Array | AggregateType.FP32) },
-            { AttributeConsts.TessLevelInner0, new AttributeInfo(AttributeConsts.TessLevelInner0, 0, 2, AggregateType.Array | AggregateType.FP32) },
-            { AttributeConsts.TessLevelInner1, new AttributeInfo(AttributeConsts.TessLevelInner0, 1, 2, AggregateType.Array | AggregateType.FP32) },
-        };
-
-        public int BaseValue { get; }
-        public int Value { get; }
-        public int Length { get; }
-        public AggregateType Type { get; }
-        public bool IsBuiltin { get; }
-        public bool IsValid => Type != AggregateType.Invalid;
-
-        public AttributeInfo(int baseValue, int index, int length, AggregateType type, bool isBuiltin = true)
-        {
-            BaseValue = baseValue;
-            Value = baseValue + index * 4;
-            Length = length;
-            Type = type;
-            IsBuiltin = isBuiltin;
-        }
-
-        public int GetInnermostIndex()
-        {
-            return (Value - BaseValue) / 4;
-        }
-
-        public static bool Validate(ShaderConfig config, int value, bool isOutAttr, bool perPatch)
-        {
-            return perPatch ? ValidatePerPatch(config, value, isOutAttr) : Validate(config, value, isOutAttr);
-        }
-
-        public static bool Validate(ShaderConfig config, int value, bool isOutAttr)
-        {
-            if (value == AttributeConsts.ViewportIndex && !config.GpuAccessor.QueryHostSupportsViewportIndex())
-            {
-                return false;
-            }
-
-            return From(config, value, isOutAttr).IsValid;
-        }
-
-        public static bool ValidatePerPatch(ShaderConfig config, int value, bool isOutAttr)
-        {
-            return FromPatch(config, value, isOutAttr).IsValid;
-        }
-
-        public static AttributeInfo From(ShaderConfig config, int value, bool isOutAttr)
-        {
-            value &= ~3;
-
-            if (value >= AttributeConsts.UserAttributeBase && value < AttributeConsts.UserAttributeEnd)
-            {
-                int location = (value - AttributeConsts.UserAttributeBase) / 16;
-
-                AggregateType elemType;
-
-                if (config.Stage == ShaderStage.Vertex && !isOutAttr)
-                {
-                    elemType = config.GpuAccessor.QueryAttributeType(location).ToAggregateType();
-                }
-                else
-                {
-                    elemType = AggregateType.FP32;
-                }
-
-                return new AttributeInfo(value & ~0xf, (value >> 2) & 3, 4, AggregateType.Vector4 | elemType, false);
-            }
-            else if (value >= AttributeConsts.FragmentOutputColorBase && value < AttributeConsts.FragmentOutputColorEnd)
-            {
-                int location = (value - AttributeConsts.FragmentOutputColorBase) / 16;
-                var elemType = config.GpuAccessor.QueryFragmentOutputType(location) switch
-                {
-                    AttributeType.Sint => AggregateType.S32,
-                    AttributeType.Uint => AggregateType.U32,
-                    _ => AggregateType.FP32
-                };
-
-                return new AttributeInfo(value & ~0xf, (value >> 2) & 3, 4, AggregateType.Vector4 | elemType, false);
-            }
-            else if (value == AttributeConsts.SupportBlockViewInverseX || value == AttributeConsts.SupportBlockViewInverseY)
-            {
-                return new AttributeInfo(value, 0, 1, AggregateType.FP32);
-            }
-            else if (_builtInAttributes.TryGetValue(value, out AttributeInfo info))
-            {
-                return info;
-            }
-
-            return new AttributeInfo(value, 0, 0, AggregateType.Invalid);
-        }
-
-        public static AttributeInfo FromPatch(ShaderConfig config, int value, bool isOutAttr)
-        {
-            value &= ~3;
-
-            if (value >= AttributeConsts.UserAttributePerPatchBase && value < AttributeConsts.UserAttributePerPatchEnd)
-            {
-                int offset = (value - AttributeConsts.UserAttributePerPatchBase) & 0xf;
-                return new AttributeInfo(value - offset, offset >> 2, 4, AggregateType.Vector4 | AggregateType.FP32, false);
-            }
-            else if (_builtInAttributesPerPatch.TryGetValue(value, out AttributeInfo info))
-            {
-                return info;
-            }
-
-            return new AttributeInfo(value, 0, 0, AggregateType.Invalid);
-        }
-
-        public static bool IsArrayBuiltIn(int attr)
-        {
-            if (attr <= AttributeConsts.TessLevelInner1 ||
-                attr == AttributeConsts.TessCoordX ||
-                attr == AttributeConsts.TessCoordY)
-            {
-                return false;
-            }
-
-            return (attr & AttributeConsts.SpecialMask) == 0;
-        }
-
-        public static bool IsArrayAttributeGlsl(ShaderStage stage, bool isOutAttr)
-        {
-            if (isOutAttr)
-            {
-                return stage == ShaderStage.TessellationControl;
-            }
-            else
-            {
-                return stage == ShaderStage.TessellationControl ||
-                       stage == ShaderStage.TessellationEvaluation ||
-                       stage == ShaderStage.Geometry;
-            }
-        }
-
-        public static bool IsArrayAttributeSpirv(ShaderStage stage, bool isOutAttr)
-        {
-            if (isOutAttr)
-            {
-                return false;
-            }
-            else
-            {
-                return stage == ShaderStage.TessellationControl ||
-                       stage == ShaderStage.TessellationEvaluation ||
-                       stage == ShaderStage.Geometry;
-            }
-        }
-    }
-}
diff --git a/Ryujinx.Graphics.Shader/Translation/EmitterContext.cs b/Ryujinx.Graphics.Shader/Translation/EmitterContext.cs
index 8f33cced..e81f6425 100644
--- a/Ryujinx.Graphics.Shader/Translation/EmitterContext.cs
+++ b/Ryujinx.Graphics.Shader/Translation/EmitterContext.cs
@@ -67,7 +67,7 @@ namespace Ryujinx.Graphics.Shader.Translation
                 (Config.Options.Flags & TranslationFlags.VertexA) == 0)
             {
                 // Vulkan requires the point size to be always written on the shader if the primitive topology is points.
-                this.Copy(Attribute(AttributeConsts.PointSize), ConstF(Config.GpuAccessor.QueryPointSize()));
+                this.Store(StorageKind.Output, IoVariable.PointSize, null, ConstF(Config.GpuAccessor.QueryPointSize()));
             }
         }
 
@@ -87,6 +87,15 @@ namespace Ryujinx.Graphics.Shader.Translation
             return dest;
         }
 
+        public Operand Add(Instruction inst, StorageKind storageKind, Operand dest = null, params Operand[] sources)
+        {
+            Operation operation = new Operation(inst, storageKind, dest, sources);
+
+            _operations.Add(operation);
+
+            return dest;
+        }
+
         public (Operand, Operand) Add(Instruction inst, (Operand, Operand) dest, params Operand[] sources)
         {
             Operand[] dests = new[] { dest.Item1, dest.Item2 };
@@ -223,30 +232,35 @@ namespace Ryujinx.Graphics.Shader.Translation
         {
             if (Config.GpuAccessor.QueryViewportTransformDisable())
             {
-                Operand x = Attribute(AttributeConsts.PositionX | AttributeConsts.LoadOutputMask);
-                Operand y = Attribute(AttributeConsts.PositionY | AttributeConsts.LoadOutputMask);
-                Operand xScale = Attribute(AttributeConsts.SupportBlockViewInverseX);
-                Operand yScale = Attribute(AttributeConsts.SupportBlockViewInverseY);
+                Operand x = this.Load(StorageKind.Output, IoVariable.Position, null, Const(0));
+                Operand y = this.Load(StorageKind.Output, IoVariable.Position, null, Const(1));
+                Operand xScale = this.Load(StorageKind.Input, IoVariable.SupportBlockViewInverse, null, Const(0));
+                Operand yScale = this.Load(StorageKind.Input, IoVariable.SupportBlockViewInverse, null, Const(1));
                 Operand negativeOne = ConstF(-1.0f);
 
-                this.Copy(Attribute(AttributeConsts.PositionX), this.FPFusedMultiplyAdd(x, xScale, negativeOne));
-                this.Copy(Attribute(AttributeConsts.PositionY), this.FPFusedMultiplyAdd(y, yScale, negativeOne));
+                this.Store(StorageKind.Output, IoVariable.Position, null, Const(0), this.FPFusedMultiplyAdd(x, xScale, negativeOne));
+                this.Store(StorageKind.Output, IoVariable.Position, null, Const(1), this.FPFusedMultiplyAdd(y, yScale, negativeOne));
             }
 
             if (Config.Options.TargetApi == TargetApi.Vulkan && Config.GpuAccessor.QueryTransformDepthMinusOneToOne())
             {
-                Operand z = Attribute(AttributeConsts.PositionZ | AttributeConsts.LoadOutputMask);
-                Operand w = Attribute(AttributeConsts.PositionW | AttributeConsts.LoadOutputMask);
+                Operand z = this.Load(StorageKind.Output, IoVariable.Position, null, Const(2));
+                Operand w = this.Load(StorageKind.Output, IoVariable.Position, null, Const(3));
                 Operand halfW = this.FPMultiply(w, ConstF(0.5f));
 
-                this.Copy(Attribute(AttributeConsts.PositionZ), this.FPFusedMultiplyAdd(z, ConstF(0.5f), halfW));
+                this.Store(StorageKind.Output, IoVariable.Position, null, Const(2), this.FPFusedMultiplyAdd(z, ConstF(0.5f), halfW));
             }
 
             if (Config.Stage != ShaderStage.Geometry && Config.HasLayerInputAttribute)
             {
                 Config.SetUsedFeature(FeatureFlags.RtLayer);
 
-                this.Copy(Attribute(AttributeConsts.Layer), Attribute(Config.GpLayerInputAttribute | AttributeConsts.LoadOutputMask));
+                int attrVecIndex = Config.GpLayerInputAttribute >> 2;
+                int attrComponentIndex = Config.GpLayerInputAttribute & 3;
+
+                Operand layer = this.Load(StorageKind.Output, IoVariable.UserDefined, null, Const(attrVecIndex), Const(attrComponentIndex));
+
+                this.Store(StorageKind.Output, IoVariable.Layer, null, layer);
             }
         }
 
@@ -255,9 +269,9 @@ namespace Ryujinx.Graphics.Shader.Translation
             if (Config.GpuAccessor.QueryViewportTransformDisable())
             {
                 oldXLocal = Local();
-                this.Copy(oldXLocal, Attribute(AttributeConsts.PositionX | AttributeConsts.LoadOutputMask));
+                this.Copy(oldXLocal, this.Load(StorageKind.Output, IoVariable.Position, null, Const(0)));
                 oldYLocal = Local();
-                this.Copy(oldYLocal, Attribute(AttributeConsts.PositionY | AttributeConsts.LoadOutputMask));
+                this.Copy(oldYLocal, this.Load(StorageKind.Output, IoVariable.Position, null, Const(1)));
             }
             else
             {
@@ -268,7 +282,7 @@ namespace Ryujinx.Graphics.Shader.Translation
             if (Config.Options.TargetApi == TargetApi.Vulkan && Config.GpuAccessor.QueryTransformDepthMinusOneToOne())
             {
                 oldZLocal = Local();
-                this.Copy(oldZLocal, Attribute(AttributeConsts.PositionZ | AttributeConsts.LoadOutputMask));
+                this.Copy(oldZLocal, this.Load(StorageKind.Output, IoVariable.Position, null, Const(2)));
             }
             else
             {
@@ -293,17 +307,30 @@ namespace Ryujinx.Graphics.Shader.Translation
             }
             else if (Config.Stage == ShaderStage.Geometry)
             {
-                void WriteOutput(int index, int primIndex)
+                void WritePositionOutput(int primIndex)
+                {
+                    Operand x = this.Load(StorageKind.Input, IoVariable.Position, Const(primIndex), Const(0));
+                    Operand y = this.Load(StorageKind.Input, IoVariable.Position, Const(primIndex), Const(1));
+                    Operand z = this.Load(StorageKind.Input, IoVariable.Position, Const(primIndex), Const(2));
+                    Operand w = this.Load(StorageKind.Input, IoVariable.Position, Const(primIndex), Const(3));
+
+                    this.Store(StorageKind.Output, IoVariable.Position, null, Const(0), x);
+                    this.Store(StorageKind.Output, IoVariable.Position, null, Const(1), y);
+                    this.Store(StorageKind.Output, IoVariable.Position, null, Const(2), z);
+                    this.Store(StorageKind.Output, IoVariable.Position, null, Const(3), w);
+                }
+
+                void WriteUserDefinedOutput(int index, int primIndex)
                 {
-                    Operand x = this.LoadAttribute(Const(index), Const(0), Const(primIndex));
-                    Operand y = this.LoadAttribute(Const(index + 4), Const(0), Const(primIndex));
-                    Operand z = this.LoadAttribute(Const(index + 8), Const(0), Const(primIndex));
-                    Operand w = this.LoadAttribute(Const(index + 12), Const(0), Const(primIndex));
-
-                    this.Copy(Attribute(index), x);
-                    this.Copy(Attribute(index + 4), y);
-                    this.Copy(Attribute(index + 8), z);
-                    this.Copy(Attribute(index + 12), w);
+                    Operand x = this.Load(StorageKind.Input, IoVariable.UserDefined, Const(primIndex), Const(index), Const(0));
+                    Operand y = this.Load(StorageKind.Input, IoVariable.UserDefined, Const(primIndex), Const(index), Const(1));
+                    Operand z = this.Load(StorageKind.Input, IoVariable.UserDefined, Const(primIndex), Const(index), Const(2));
+                    Operand w = this.Load(StorageKind.Input, IoVariable.UserDefined, Const(primIndex), Const(index), Const(3));
+
+                    this.Store(StorageKind.Output, IoVariable.UserDefined, null, Const(index), Const(0), x);
+                    this.Store(StorageKind.Output, IoVariable.UserDefined, null, Const(index), Const(1), y);
+                    this.Store(StorageKind.Output, IoVariable.UserDefined, null, Const(index), Const(2), z);
+                    this.Store(StorageKind.Output, IoVariable.UserDefined, null, Const(index), Const(3), w);
                 }
 
                 if (Config.GpPassthrough && !Config.GpuAccessor.QueryHostSupportsGeometryShaderPassthrough())
@@ -312,13 +339,13 @@ namespace Ryujinx.Graphics.Shader.Translation
 
                     for (int primIndex = 0; primIndex < inputVertices; primIndex++)
                     {
-                        WriteOutput(AttributeConsts.PositionX, primIndex);
+                        WritePositionOutput(primIndex);
 
                         int passthroughAttributes = Config.PassthroughAttributes;
                         while (passthroughAttributes != 0)
                         {
                             int index = BitOperations.TrailingZeroCount(passthroughAttributes);
-                            WriteOutput(AttributeConsts.UserAttributeBase + index * 16, primIndex);
+                            WriteUserDefinedOutput(index, primIndex);
                             Config.SetOutputUserAttribute(index);
                             passthroughAttributes &= ~(1 << index);
                         }
@@ -337,11 +364,9 @@ namespace Ryujinx.Graphics.Shader.Translation
 
                 if (Config.OmapDepth)
                 {
-                    Operand dest = Attribute(AttributeConsts.FragmentOutputDepth);
-
                     Operand src = Register(Config.GetDepthRegister(), RegisterType.Gpr);
 
-                    this.Copy(dest, src);
+                    this.Store(StorageKind.Output, IoVariable.FragmentOutputDepth, null, src);
                 }
 
                 AlphaTestOp alphaTestOp = Config.GpuAccessor.QueryAlphaTestCompare();
@@ -390,32 +415,30 @@ namespace Ryujinx.Graphics.Shader.Translation
                             continue;
                         }
 
-                        int fragmentOutputColorAttr = AttributeConsts.FragmentOutputColorBase + rtIndex * 16;
-
                         Operand src = Register(regIndexBase + component, RegisterType.Gpr);
 
                         // Perform B <-> R swap if needed, for BGRA formats (not supported on OpenGL).
                         if (!supportsBgra && (component == 0 || component == 2))
                         {
-                            Operand isBgra = Attribute(AttributeConsts.FragmentOutputIsBgraBase + rtIndex * 4);
+                            Operand isBgra = this.Load(StorageKind.Input, IoVariable.FragmentOutputIsBgra, null, Const(rtIndex));
 
                             Operand lblIsBgra = Label();
                             Operand lblEnd = Label();
 
                             this.BranchIfTrue(lblIsBgra, isBgra);
 
-                            this.Copy(Attribute(fragmentOutputColorAttr + component * 4), src);
+                            this.Store(StorageKind.Output, IoVariable.FragmentOutputColor, null, Const(rtIndex), Const(component), src);
                             this.Branch(lblEnd);
 
                             MarkLabel(lblIsBgra);
 
-                            this.Copy(Attribute(fragmentOutputColorAttr + (2 - component) * 4), src);
+                            this.Store(StorageKind.Output, IoVariable.FragmentOutputColor, null, Const(rtIndex), Const(2 - component), src);
 
                             MarkLabel(lblEnd);
                         }
                         else
                         {
-                            this.Copy(Attribute(fragmentOutputColorAttr + component * 4), src);
+                            this.Store(StorageKind.Output, IoVariable.FragmentOutputColor, null, Const(rtIndex), Const(component), src);
                         }
                     }
 
@@ -441,8 +464,11 @@ namespace Ryujinx.Graphics.Shader.Translation
             // 11 01 01 01 01 00 00 00
             Operand ditherMask = Const(unchecked((int)0xfbb99110u));
 
-            Operand x = this.BitwiseAnd(this.FP32ConvertToU32(Attribute(AttributeConsts.PositionX)), Const(1));
-            Operand y = this.BitwiseAnd(this.FP32ConvertToU32(Attribute(AttributeConsts.PositionY)), Const(1));
+            Operand fragCoordX = this.Load(StorageKind.Input, IoVariable.FragmentCoord, null, Const(0));
+            Operand fragCoordY = this.Load(StorageKind.Input, IoVariable.FragmentCoord, null, Const(1));
+
+            Operand x = this.BitwiseAnd(this.FP32ConvertToU32(fragCoordX), Const(1));
+            Operand y = this.BitwiseAnd(this.FP32ConvertToU32(fragCoordY), Const(1));
             Operand xy = this.BitwiseOr(x, this.ShiftLeft(y, Const(1)));
 
             Operand alpha = Register(3, RegisterType.Gpr);
diff --git a/Ryujinx.Graphics.Shader/Translation/EmitterContextInsts.cs b/Ryujinx.Graphics.Shader/Translation/EmitterContextInsts.cs
index 1fb60508..93748249 100644
--- a/Ryujinx.Graphics.Shader/Translation/EmitterContextInsts.cs
+++ b/Ryujinx.Graphics.Shader/Translation/EmitterContextInsts.cs
@@ -7,54 +7,54 @@ namespace Ryujinx.Graphics.Shader.Translation
 {
     static class EmitterContextInsts
     {
-        public static Operand AtomicAdd(this EmitterContext context, Instruction mr, Operand a, Operand b, Operand c)
+        public static Operand AtomicAdd(this EmitterContext context, StorageKind storageKind, Operand a, Operand b, Operand c)
         {
-            return context.Add(Instruction.AtomicAdd | mr, Local(), a, b, c);
+            return context.Add(Instruction.AtomicAdd, storageKind, Local(), a, b, c);
         }
 
-        public static Operand AtomicAnd(this EmitterContext context, Instruction mr, Operand a, Operand b, Operand c)
+        public static Operand AtomicAnd(this EmitterContext context, StorageKind storageKind, Operand a, Operand b, Operand c)
         {
-            return context.Add(Instruction.AtomicAnd | mr, Local(), a, b, c);
+            return context.Add(Instruction.AtomicAnd, storageKind, Local(), a, b, c);
         }
 
-        public static Operand AtomicCompareAndSwap(this EmitterContext context, Instruction mr, Operand a, Operand b, Operand c, Operand d)
+        public static Operand AtomicCompareAndSwap(this EmitterContext context, StorageKind storageKind, Operand a, Operand b, Operand c, Operand d)
         {
-            return context.Add(Instruction.AtomicCompareAndSwap | mr, Local(), a, b, c, d);
+            return context.Add(Instruction.AtomicCompareAndSwap, storageKind, Local(), a, b, c, d);
         }
 
-        public static Operand AtomicMaxS32(this EmitterContext context, Instruction mr, Operand a, Operand b, Operand c)
+        public static Operand AtomicMaxS32(this EmitterContext context, StorageKind storageKind, Operand a, Operand b, Operand c)
         {
-            return context.Add(Instruction.AtomicMaxS32 | mr, Local(), a, b, c);
+            return context.Add(Instruction.AtomicMaxS32, storageKind, Local(), a, b, c);
         }
 
-        public static Operand AtomicMaxU32(this EmitterContext context, Instruction mr, Operand a, Operand b, Operand c)
+        public static Operand AtomicMaxU32(this EmitterContext context, StorageKind storageKind, Operand a, Operand b, Operand c)
         {
-            return context.Add(Instruction.AtomicMaxU32 | mr, Local(), a, b, c);
+            return context.Add(Instruction.AtomicMaxU32, storageKind, Local(), a, b, c);
         }
 
-        public static Operand AtomicMinS32(this EmitterContext context, Instruction mr, Operand a, Operand b, Operand c)
+        public static Operand AtomicMinS32(this EmitterContext context, StorageKind storageKind, Operand a, Operand b, Operand c)
         {
-            return context.Add(Instruction.AtomicMinS32 | mr, Local(), a, b, c);
+            return context.Add(Instruction.AtomicMinS32, storageKind, Local(), a, b, c);
         }
 
-        public static Operand AtomicMinU32(this EmitterContext context, Instruction mr, Operand a, Operand b, Operand c)
+        public static Operand AtomicMinU32(this EmitterContext context, StorageKind storageKind, Operand a, Operand b, Operand c)
         {
-            return context.Add(Instruction.AtomicMinU32 | mr, Local(), a, b, c);
+            return context.Add(Instruction.AtomicMinU32, storageKind, Local(), a, b, c);
         }
 
-        public static Operand AtomicOr(this EmitterContext context, Instruction mr, Operand a, Operand b, Operand c)
+        public static Operand AtomicOr(this EmitterContext context, StorageKind storageKind, Operand a, Operand b, Operand c)
         {
-            return context.Add(Instruction.AtomicOr | mr, Local(), a, b, c);
+            return context.Add(Instruction.AtomicOr, storageKind, Local(), a, b, c);
         }
 
-        public static Operand AtomicSwap(this EmitterContext context, Instruction mr, Operand a, Operand b, Operand c)
+        public static Operand AtomicSwap(this EmitterContext context, StorageKind storageKind, Operand a, Operand b, Operand c)
         {
-            return context.Add(Instruction.AtomicSwap | mr, Local(), a, b, c);
+            return context.Add(Instruction.AtomicSwap, storageKind, Local(), a, b, c);
         }
 
-        public static Operand AtomicXor(this EmitterContext context, Instruction mr, Operand a, Operand b, Operand c)
+        public static Operand AtomicXor(this EmitterContext context, StorageKind storageKind, Operand a, Operand b, Operand c)
         {
-            return context.Add(Instruction.AtomicXor | mr, Local(), a, b, c);
+            return context.Add(Instruction.AtomicXor, storageKind, Local(), a, b, c);
         }
 
         public static Operand Ballot(this EmitterContext context, Operand a)
@@ -549,9 +549,36 @@ namespace Ryujinx.Graphics.Shader.Translation
             return context.Add(fpType | Instruction.IsNan, Local(), a);
         }
 
-        public static Operand LoadAttribute(this EmitterContext context, Operand a, Operand b, Operand c)
+        public static Operand Load(this EmitterContext context, StorageKind storageKind, IoVariable ioVariable, Operand primVertex = null)
         {
-            return context.Add(Instruction.LoadAttribute, Local(), a, b, c);
+            return primVertex != null
+                ? context.Add(Instruction.Load, storageKind, Local(), Const((int)ioVariable), primVertex)
+                : context.Add(Instruction.Load, storageKind, Local(), Const((int)ioVariable));
+        }
+
+        public static Operand Load(
+            this EmitterContext context,
+            StorageKind storageKind,
+            IoVariable ioVariable,
+            Operand primVertex,
+            Operand elemIndex)
+        {
+            return primVertex != null
+                ? context.Add(Instruction.Load, storageKind, Local(), Const((int)ioVariable), primVertex, elemIndex)
+                : context.Add(Instruction.Load, storageKind, Local(), Const((int)ioVariable), elemIndex);
+        }
+
+        public static Operand Load(
+            this EmitterContext context,
+            StorageKind storageKind,
+            IoVariable ioVariable,
+            Operand primVertex,
+            Operand arrayIndex,
+            Operand elemIndex)
+        {
+            return primVertex != null
+                ? context.Add(Instruction.Load, storageKind, Local(), Const((int)ioVariable), primVertex, arrayIndex, elemIndex)
+                : context.Add(Instruction.Load, storageKind, Local(), Const((int)ioVariable), arrayIndex, elemIndex);
         }
 
         public static Operand LoadConstant(this EmitterContext context, Operand a, Operand b)
@@ -662,9 +689,43 @@ namespace Ryujinx.Graphics.Shader.Translation
             return context.Add(Instruction.ShuffleXor, (Local(), Local()), a, b, c);
         }
 
-        public static Operand StoreAttribute(this EmitterContext context, Operand a, Operand b, Operand c)
+        public static Operand Store(
+            this EmitterContext context,
+            StorageKind storageKind,
+            IoVariable ioVariable,
+            Operand invocationId,
+            Operand value)
+        {
+            return invocationId != null
+                ? context.Add(Instruction.Store, storageKind, null, Const((int)ioVariable), invocationId, value)
+                : context.Add(Instruction.Store, storageKind, null, Const((int)ioVariable), value);
+        }
+
+        public static Operand Store(
+            this EmitterContext context,
+            StorageKind storageKind,
+            IoVariable ioVariable,
+            Operand invocationId,
+            Operand elemIndex,
+            Operand value)
+        {
+            return invocationId != null
+                ? context.Add(Instruction.Store, storageKind, null, Const((int)ioVariable), invocationId, elemIndex, value)
+                : context.Add(Instruction.Store, storageKind, null, Const((int)ioVariable), elemIndex, value);
+        }
+
+        public static Operand Store(
+            this EmitterContext context,
+            StorageKind storageKind,
+            IoVariable ioVariable,
+            Operand invocationId,
+            Operand arrayIndex,
+            Operand elemIndex,
+            Operand value)
         {
-            return context.Add(Instruction.StoreAttribute, null, a, b, c);
+            return invocationId != null
+                ? context.Add(Instruction.Store, storageKind, null, Const((int)ioVariable), invocationId, arrayIndex, elemIndex, value)
+                : context.Add(Instruction.Store, storageKind, null, Const((int)ioVariable), arrayIndex, elemIndex, value);
         }
 
         public static Operand StoreGlobal(this EmitterContext context, Operand a, Operand b, Operand c)
diff --git a/Ryujinx.Graphics.Shader/Translation/GlobalMemory.cs b/Ryujinx.Graphics.Shader/Translation/GlobalMemory.cs
index 3915c0d5..774a128d 100644
--- a/Ryujinx.Graphics.Shader/Translation/GlobalMemory.cs
+++ b/Ryujinx.Graphics.Shader/Translation/GlobalMemory.cs
@@ -16,20 +16,15 @@ namespace Ryujinx.Graphics.Shader.Translation
         public const int UbeDescsSize  = StorageDescSize * UbeMaxCount;
         public const int UbeFirstCbuf  = 8;
 
-        public static bool UsesGlobalMemory(Instruction inst)
+        public static bool UsesGlobalMemory(Instruction inst, StorageKind storageKind)
         {
-            return (inst.IsAtomic() && IsGlobalMr(inst)) ||
+            return (inst.IsAtomic() && storageKind == StorageKind.GlobalMemory) ||
                     inst == Instruction.LoadGlobal ||
                     inst == Instruction.StoreGlobal ||
                     inst == Instruction.StoreGlobal16 ||
                     inst == Instruction.StoreGlobal8;
         }
 
-        private static bool IsGlobalMr(Instruction inst)
-        {
-            return (inst & Instruction.MrMask) == Instruction.MrGlobal;
-        }
-
         public static int GetStorageCbOffset(ShaderStage stage, int slot)
         {
             return GetStorageBaseCbOffset(stage) + slot * StorageDescSize;
diff --git a/Ryujinx.Graphics.Shader/Translation/Optimizations/GlobalToStorage.cs b/Ryujinx.Graphics.Shader/Translation/Optimizations/GlobalToStorage.cs
index c280a6d8..2a4070e0 100644
--- a/Ryujinx.Graphics.Shader/Translation/Optimizations/GlobalToStorage.cs
+++ b/Ryujinx.Graphics.Shader/Translation/Optimizations/GlobalToStorage.cs
@@ -45,7 +45,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
                     continue;
                 }
 
-                if (UsesGlobalMemory(operation.Inst))
+                if (UsesGlobalMemory(operation.Inst, operation.StorageKind))
                 {
                     Operand source = operation.GetSource(0);
 
@@ -104,9 +104,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
 
             if (isAtomic)
             {
-                Instruction inst = (operation.Inst & ~Instruction.MrMask) | Instruction.MrStorage;
-
-                storageOp = new Operation(inst, operation.Dest, sources);
+                storageOp = new Operation(operation.Inst, StorageKind.StorageBuffer, operation.Dest, sources);
             }
             else if (operation.Inst == Instruction.LoadGlobal)
             {
diff --git a/Ryujinx.Graphics.Shader/Translation/Optimizations/Optimizer.cs b/Ryujinx.Graphics.Shader/Translation/Optimizations/Optimizer.cs
index a2219b36..bae774ee 100644
--- a/Ryujinx.Graphics.Shader/Translation/Optimizations/Optimizer.cs
+++ b/Ryujinx.Graphics.Shader/Translation/Optimizations/Optimizer.cs
@@ -170,10 +170,8 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
                 return false;
             }
 
-            return x.Type == OperandType.Attribute ||
-                   x.Type == OperandType.AttributePerPatch ||
-                   x.Type == OperandType.Constant ||
-                   x.Type == OperandType.ConstantBuffer;
+            // TODO: Handle Load operations with the same storage and the same constant parameters.
+            return x.Type == OperandType.Constant || x.Type == OperandType.ConstantBuffer;
         }
 
         private static bool PropagatePack(Operation packOp)
diff --git a/Ryujinx.Graphics.Shader/Translation/Rewriter.cs b/Ryujinx.Graphics.Shader/Translation/Rewriter.cs
index 3ec4e49a..91e7ace1 100644
--- a/Ryujinx.Graphics.Shader/Translation/Rewriter.cs
+++ b/Ryujinx.Graphics.Shader/Translation/Rewriter.cs
@@ -34,7 +34,7 @@ namespace Ryujinx.Graphics.Shader.Translation
                     {
                         if (hasConstantBufferDrawParameters)
                         {
-                            if (ReplaceConstantBufferWithDrawParameters(operation))
+                            if (ReplaceConstantBufferWithDrawParameters(node, operation))
                             {
                                 config.SetUsedFeature(FeatureFlags.DrawParameters);
                             }
@@ -61,7 +61,7 @@ namespace Ryujinx.Graphics.Shader.Translation
 
                         nextNode = node.Next;
                     }
-                    else if (UsesGlobalMemory(operation.Inst))
+                    else if (UsesGlobalMemory(operation.Inst, operation.StorageKind))
                     {
                         nextNode = RewriteGlobalAccess(node, config)?.Next ?? nextNode;
                     }
@@ -169,9 +169,7 @@ namespace Ryujinx.Graphics.Shader.Translation
 
                 if (isAtomic)
                 {
-                    Instruction inst = (operation.Inst & ~Instruction.MrMask) | Instruction.MrStorage;
-
-                    storageOp = new Operation(inst, operation.Dest, sources);
+                    storageOp = new Operation(operation.Inst, StorageKind.StorageBuffer, operation.Dest, sources);
                 }
                 else if (operation.Inst == Instruction.LoadGlobal)
                 {
@@ -708,8 +706,15 @@ namespace Ryujinx.Graphics.Shader.Translation
             return node;
         }
 
-        private static bool ReplaceConstantBufferWithDrawParameters(Operation operation)
+        private static bool ReplaceConstantBufferWithDrawParameters(LinkedListNode<INode> node, Operation operation)
         {
+            Operand GenerateLoad(IoVariable ioVariable)
+            {
+                Operand value = Local();
+                node.List.AddBefore(node, new Operation(Instruction.Load, StorageKind.Input, value, Const((int)ioVariable)));
+                return value;
+            }
+
             bool modified = false;
 
             for (int srcIndex = 0; srcIndex < operation.SourcesCount; srcIndex++)
@@ -721,15 +726,15 @@ namespace Ryujinx.Graphics.Shader.Translation
                     switch (src.GetCbufOffset())
                     {
                         case Constants.NvnBaseVertexByteOffset / 4:
-                            operation.SetSource(srcIndex, Attribute(AttributeConsts.BaseVertex));
+                            operation.SetSource(srcIndex, GenerateLoad(IoVariable.BaseVertex));
                             modified = true;
                             break;
                         case Constants.NvnBaseInstanceByteOffset / 4:
-                            operation.SetSource(srcIndex, Attribute(AttributeConsts.BaseInstance));
+                            operation.SetSource(srcIndex, GenerateLoad(IoVariable.BaseInstance));
                             modified = true;
                             break;
                         case Constants.NvnDrawIndexByteOffset / 4:
-                            operation.SetSource(srcIndex, Attribute(AttributeConsts.DrawIndex));
+                            operation.SetSource(srcIndex, GenerateLoad(IoVariable.DrawIndex));
                             modified = true;
                             break;
                     }
diff --git a/Ryujinx.Graphics.Shader/Translation/ShaderConfig.cs b/Ryujinx.Graphics.Shader/Translation/ShaderConfig.cs
index 15eb7ed1..22f5a671 100644
--- a/Ryujinx.Graphics.Shader/Translation/ShaderConfig.cs
+++ b/Ryujinx.Graphics.Shader/Translation/ShaderConfig.cs
@@ -41,6 +41,46 @@ namespace Ryujinx.Graphics.Shader.Translation
 
         public bool TransformFeedbackEnabled { get; }
 
+        private TransformFeedbackOutput[] _transformFeedbackOutputs;
+
+        readonly struct TransformFeedbackVariable : IEquatable<TransformFeedbackVariable>
+        {
+            public IoVariable IoVariable { get; }
+            public int Location { get; }
+            public int Component { get; }
+
+            public TransformFeedbackVariable(IoVariable ioVariable, int location = 0, int component = 0)
+            {
+                IoVariable = ioVariable;
+                Location = location;
+                Component = component;
+            }
+
+            public override bool Equals(object other)
+            {
+                return other is TransformFeedbackVariable tfbVar && Equals(tfbVar);
+            }
+
+            public bool Equals(TransformFeedbackVariable other)
+            {
+                return IoVariable == other.IoVariable &&
+                    Location == other.Location &&
+                    Component == other.Component;
+            }
+
+            public override int GetHashCode()
+            {
+                return (int)IoVariable | (Location << 8) | (Component << 16);
+            }
+
+            public override string ToString()
+            {
+                return $"{IoVariable}.{Location}.{Component}";
+            }
+        }
+
+        private readonly Dictionary<TransformFeedbackVariable, TransformFeedbackOutput> _transformFeedbackDefinitions;
+
         public int Size { get; private set; }
 
         public byte ClipDistancesWritten { get; private set; }
@@ -102,6 +142,8 @@ namespace Ryujinx.Graphics.Shader.Translation
             GpuAccessor = gpuAccessor;
             Options     = options;
 
+            _transformFeedbackDefinitions = new Dictionary<TransformFeedbackVariable, TransformFeedbackOutput>();
+
             AccessibleStorageBuffersMask  = (1 << GlobalMemory.StorageMaxCount) - 1;
             AccessibleConstantBuffersMask = (1 << GlobalMemory.UbeMaxCount) - 1;
 
@@ -147,6 +189,173 @@ namespace Ryujinx.Graphics.Shader.Translation
             LastInVertexPipeline     = header.Stage < ShaderStage.Fragment;
         }
 
+        private void EnsureTransformFeedbackInitialized()
+        {
+            if (HasTransformFeedbackOutputs() && _transformFeedbackOutputs == null)
+            {
+                TransformFeedbackOutput[] transformFeedbackOutputs = new TransformFeedbackOutput[0xc0];
+                ulong vecMap = 0UL;
+
+                for (int tfbIndex = 0; tfbIndex < 4; tfbIndex++)
+                {
+                    var locations = GpuAccessor.QueryTransformFeedbackVaryingLocations(tfbIndex);
+                    var stride = GpuAccessor.QueryTransformFeedbackStride(tfbIndex);
+
+                    for (int i = 0; i < locations.Length; i++)
+                    {
+                        byte wordOffset = locations[i];
+                        if (wordOffset < 0xc0)
+                        {
+                            transformFeedbackOutputs[wordOffset] = new TransformFeedbackOutput(tfbIndex, i * 4, stride);
+                            vecMap |= 1UL << (wordOffset / 4);
+                        }
+                    }
+                }
+
+                _transformFeedbackOutputs = transformFeedbackOutputs;
+
+                while (vecMap != 0)
+                {
+                    int vecIndex = BitOperations.TrailingZeroCount(vecMap);
+
+                    for (int subIndex = 0; subIndex < 4; subIndex++)
+                    {
+                        int wordOffset = vecIndex * 4 + subIndex;
+                        int byteOffset = wordOffset * 4;
+
+                        if (transformFeedbackOutputs[wordOffset].Valid)
+                        {
+                            IoVariable ioVariable = Instructions.AttributeMap.GetIoVariable(this, byteOffset, out int location);
+                            int component = 0;
+
+                            if (HasPerLocationInputOrOutputComponent(ioVariable, location, subIndex, isOutput: true))
+                            {
+                                component = subIndex;
+                            }
+
+                            var transformFeedbackVariable = new TransformFeedbackVariable(ioVariable, location, component);
+                            _transformFeedbackDefinitions.TryAdd(transformFeedbackVariable, transformFeedbackOutputs[wordOffset]);
+                        }
+                    }
+
+                    vecMap &= ~(1UL << vecIndex);
+                }
+            }
+        }
+
+        public TransformFeedbackOutput[] GetTransformFeedbackOutputs()
+        {
+            EnsureTransformFeedbackInitialized();
+            return _transformFeedbackOutputs;
+        }
+
+        public bool TryGetTransformFeedbackOutput(IoVariable ioVariable, int location, int component, out TransformFeedbackOutput transformFeedbackOutput)
+        {
+            EnsureTransformFeedbackInitialized();
+            var transformFeedbackVariable = new TransformFeedbackVariable(ioVariable, location, component);
+            return _transformFeedbackDefinitions.TryGetValue(transformFeedbackVariable, out transformFeedbackOutput);
+        }
+
+        private bool HasTransformFeedbackOutputs()
+        {
+            return TransformFeedbackEnabled && (LastInVertexPipeline || Stage == ShaderStage.Fragment);
+        }
+
+        public bool HasTransformFeedbackOutputs(bool isOutput)
+        {
+            return TransformFeedbackEnabled && ((isOutput && LastInVertexPipeline) || (!isOutput && Stage == ShaderStage.Fragment));
+        }
+
+        public bool HasPerLocationInputOrOutput(IoVariable ioVariable, bool isOutput)
+        {
+            if (ioVariable == IoVariable.UserDefined)
+            {
+                return (!isOutput && !UsedFeatures.HasFlag(FeatureFlags.IaIndexing)) ||
+                       (isOutput && !UsedFeatures.HasFlag(FeatureFlags.OaIndexing));
+            }
+
+            return ioVariable == IoVariable.FragmentOutputColor;
+        }
+
+        public bool HasPerLocationInputOrOutputComponent(IoVariable ioVariable, int location, int component, bool isOutput)
+        {
+            if (ioVariable != IoVariable.UserDefined || !HasTransformFeedbackOutputs(isOutput))
+            {
+                return false;
+            }
+
+            return GetTransformFeedbackOutputComponents(location, component) == 1;
+        }
+
+        public TransformFeedbackOutput GetTransformFeedbackOutput(int wordOffset)
+        {
+            EnsureTransformFeedbackInitialized();
+
+            return _transformFeedbackOutputs[wordOffset];
+        }
+
+        public TransformFeedbackOutput GetTransformFeedbackOutput(int location, int component)
+        {
+            return GetTransformFeedbackOutput((AttributeConsts.UserAttributeBase / 4) + location * 4 + component);
+        }
+
+        public int GetTransformFeedbackOutputComponents(int location, int component)
+        {
+            EnsureTransformFeedbackInitialized();
+
+            int baseIndex = (AttributeConsts.UserAttributeBase / 4) + location * 4;
+            int index = baseIndex + component;
+            int count = 1;
+
+            for (; count < 4; count++)
+            {
+                ref var prev = ref _transformFeedbackOutputs[baseIndex + count - 1];
+                ref var curr = ref _transformFeedbackOutputs[baseIndex + count];
+
+                int prevOffset = prev.Offset;
+                int currOffset = curr.Offset;
+
+                if (!prev.Valid || !curr.Valid || prevOffset + 4 != currOffset)
+                {
+                    break;
+                }
+            }
+
+            if (baseIndex + count <= index)
+            {
+                return 1;
+            }
+
+            return count;
+        }
+
+        public AggregateType GetFragmentOutputColorType(int location)
+        {
+            return AggregateType.Vector4 | GpuAccessor.QueryFragmentOutputType(location).ToAggregateType();
+        }
+
+        public AggregateType GetUserDefinedType(int location, bool isOutput)
+        {
+            if ((!isOutput && UsedFeatures.HasFlag(FeatureFlags.IaIndexing)) ||
+                (isOutput && UsedFeatures.HasFlag(FeatureFlags.OaIndexing)))
+            {
+                return AggregateType.Array | AggregateType.Vector4 | AggregateType.FP32;
+            }
+
+            AggregateType type = AggregateType.Vector4;
+
+            if (Stage == ShaderStage.Vertex && !isOutput)
+            {
+                type |= GpuAccessor.QueryAttributeType(location).ToAggregateType();
+            }
+            else
+            {
+                type |= AggregateType.FP32;
+            }
+
+            return type;
+        }
+
         public int GetDepthRegister()
         {
             // The depth register is always two registers after the last color output.
@@ -184,7 +393,7 @@ namespace Ryujinx.Graphics.Shader.Translation
             return format;
         }
 
-        private bool FormatSupportsAtomic(TextureFormat format)
+        private static bool FormatSupportsAtomic(TextureFormat format)
         {
             return format == TextureFormat.R32Sint || format == TextureFormat.R32Uint;
         }
diff --git a/Ryujinx.Graphics.Shader/Translation/ShaderIdentifier.cs b/Ryujinx.Graphics.Shader/Translation/ShaderIdentifier.cs
index 206718f2..53f1e847 100644
--- a/Ryujinx.Graphics.Shader/Translation/ShaderIdentifier.cs
+++ b/Ryujinx.Graphics.Shader/Translation/ShaderIdentifier.cs
@@ -53,40 +53,80 @@ namespace Ryujinx.Graphics.Shader.Translation
                         return false;
                     }
 
-                    if (operation.Inst == Instruction.StoreAttribute)
+                    if (operation.Inst == Instruction.Store && operation.StorageKind == StorageKind.Output)
                     {
-                        return false;
-                    }
-
-                    if (operation.Inst == Instruction.Copy && operation.Dest.Type == OperandType.Attribute)
-                    {
-                        Operand src = operation.GetSource(0);
+                        Operand src = operation.GetSource(operation.SourcesCount - 1);
+                        Operation srcAttributeAsgOp = null;
 
-                        if (src.Type == OperandType.LocalVariable && src.AsgOp is Operation asgOp && asgOp.Inst == Instruction.LoadAttribute)
+                        if (src.Type == OperandType.LocalVariable &&
+                            src.AsgOp is Operation asgOp &&
+                            asgOp.Inst == Instruction.Load &&
+                            asgOp.StorageKind.IsInputOrOutput())
                         {
-                            src = Attribute(asgOp.GetSource(0).Value);
+                            if (asgOp.StorageKind != StorageKind.Input)
+                            {
+                                return false;
+                            }
+
+                            srcAttributeAsgOp = asgOp;
                         }
 
-                        if (src.Type == OperandType.Attribute)
+                        if (srcAttributeAsgOp != null)
                         {
-                            if (operation.Dest.Value == AttributeConsts.Layer)
+                            IoVariable dstAttribute = (IoVariable)operation.GetSource(0).Value;
+                            IoVariable srcAttribute = (IoVariable)srcAttributeAsgOp.GetSource(0).Value;
+
+                            if (dstAttribute == IoVariable.Layer && srcAttribute == IoVariable.UserDefined)
                             {
-                                if ((src.Value & AttributeConsts.LoadOutputMask) != 0)
+                                if (srcAttributeAsgOp.SourcesCount != 4)
                                 {
                                     return false;
                                 }
 
                                 writesLayer = true;
-                                layerInputAttr = src.Value;
+                                layerInputAttr = srcAttributeAsgOp.GetSource(1).Value * 4 + srcAttributeAsgOp.GetSource(3).Value;;
                             }
-                            else if (src.Value != operation.Dest.Value)
+                            else
                             {
-                                return false;
+                                if (dstAttribute != srcAttribute)
+                                {
+                                    return false;
+                                }
+
+                                int inputsCount = operation.SourcesCount - 2;
+
+                                if (dstAttribute == IoVariable.UserDefined)
+                                {
+                                    if (operation.GetSource(1).Value != srcAttributeAsgOp.GetSource(1).Value)
+                                    {
+                                        return false;
+                                    }
+
+                                    inputsCount--;
+                                }
+
+                                for (int i = 0; i < inputsCount; i++)
+                                {
+                                    int dstIndex = operation.SourcesCount - 2 - i;
+                                    int srcIndex = srcAttributeAsgOp.SourcesCount - 1 - i;
+
+                                    if ((dstIndex | srcIndex) < 0)
+                                    {
+                                        return false;
+                                    }
+
+                                    if (operation.GetSource(dstIndex).Type != OperandType.Constant ||
+                                        srcAttributeAsgOp.GetSource(srcIndex).Type != OperandType.Constant ||
+                                        operation.GetSource(dstIndex).Value != srcAttributeAsgOp.GetSource(srcIndex).Value)
+                                    {
+                                        return false;
+                                    }
+                                }
                             }
                         }
                         else if (src.Type == OperandType.Constant)
                         {
-                            int dstComponent = (operation.Dest.Value >> 2) & 3;
+                            int dstComponent = operation.GetSource(operation.SourcesCount - 2).Value;
                             float expectedValue = dstComponent == 3 ? 1f : 0f;
 
                             if (src.AsFloat() != expectedValue)
diff --git a/Ryujinx.Graphics.Shader/Translation/Translator.cs b/Ryujinx.Graphics.Shader/Translation/Translator.cs
index 6a123045..77d3b568 100644
--- a/Ryujinx.Graphics.Shader/Translation/Translator.cs
+++ b/Ryujinx.Graphics.Shader/Translation/Translator.cs
@@ -177,7 +177,7 @@ namespace Ryujinx.Graphics.Shader.Translation
 
             if (config.Stage == ShaderStage.Vertex)
             {
-                InitializeOutput(context, AttributeConsts.PositionX, perPatch: false);
+                InitializePositionOutput(context);
             }
 
             UInt128 usedAttributes = context.Config.NextInputAttributesComponents;
@@ -194,20 +194,23 @@ namespace Ryujinx.Graphics.Shader.Translation
                     continue;
                 }
 
-                InitializeOutputComponent(context, AttributeConsts.UserAttributeBase + index * 4, perPatch: false);
+                InitializeOutputComponent(context, vecIndex, index & 3, perPatch: false);
             }
 
             if (context.Config.NextUsedInputAttributesPerPatch != null)
             {
                 foreach (int vecIndex in context.Config.NextUsedInputAttributesPerPatch.Order())
                 {
-                    InitializeOutput(context, AttributeConsts.UserAttributePerPatchBase + vecIndex * 16, perPatch: true);
+                    InitializeOutput(context, vecIndex, perPatch: true);
                 }
             }
 
             if (config.NextUsesFixedFuncAttributes)
             {
-                for (int i = 0; i < 4 + AttributeConsts.TexCoordCount; i++)
+                bool supportsLayerFromVertexOrTess = config.GpuAccessor.QueryHostSupportsLayerVertexTessellation();
+                int fixedStartAttr = supportsLayerFromVertexOrTess ? 0 : 1;
+
+                for (int i = fixedStartAttr; i < fixedStartAttr + 5 + AttributeConsts.TexCoordCount; i++)
                 {
                     int index = config.GetFreeUserAttribute(isOutput: true, i);
                     if (index < 0)
@@ -215,26 +218,58 @@ namespace Ryujinx.Graphics.Shader.Translation
                         break;
                     }
 
-                    InitializeOutput(context, AttributeConsts.UserAttributeBase + index * 16, perPatch: false);
+                    InitializeOutput(context, index, perPatch: false);
 
                     config.SetOutputUserAttributeFixedFunc(index);
                 }
             }
         }
 
-        private static void InitializeOutput(EmitterContext context, int baseAttr, bool perPatch)
+        private static void InitializePositionOutput(EmitterContext context)
         {
             for (int c = 0; c < 4; c++)
             {
-                int attrOffset = baseAttr + c * 4;
-                InitializeOutputComponent(context, attrOffset, perPatch);
+                context.Store(StorageKind.Output, IoVariable.Position, null, Const(c), ConstF(c == 3 ? 1f : 0f));
             }
         }
 
-        private static void InitializeOutputComponent(EmitterContext context, int attrOffset, bool perPatch)
+        private static void InitializeOutput(EmitterContext context, int location, bool perPatch)
         {
-            int c = (attrOffset >> 2) & 3;
-            context.Copy(perPatch ? AttributePerPatch(attrOffset) : Attribute(attrOffset), ConstF(c == 3 ? 1f : 0f));
+            for (int c = 0; c < 4; c++)
+            {
+                InitializeOutputComponent(context, location, c, perPatch);
+            }
+        }
+
+        private static void InitializeOutputComponent(EmitterContext context, int location, int c, bool perPatch)
+        {
+            StorageKind storageKind = perPatch ? StorageKind.OutputPerPatch : StorageKind.Output;
+
+            if (context.Config.UsedFeatures.HasFlag(FeatureFlags.OaIndexing))
+            {
+                Operand invocationId = null;
+
+                if (context.Config.Stage == ShaderStage.TessellationControl && !perPatch)
+                {
+                    invocationId = context.Load(StorageKind.Input, IoVariable.InvocationId);
+                }
+
+                int index = location * 4 + c;
+
+                context.Store(storageKind, IoVariable.UserDefined, invocationId, Const(index), ConstF(c == 3 ? 1f : 0f));
+            }
+            else
+            {
+                if (context.Config.Stage == ShaderStage.TessellationControl && !perPatch)
+                {
+                    Operand invocationId = context.Load(StorageKind.Input, IoVariable.InvocationId);
+                    context.Store(storageKind, IoVariable.UserDefined, Const(location), invocationId, Const(c), ConstF(c == 3 ? 1f : 0f));
+                }
+                else
+                {
+                    context.Store(storageKind, IoVariable.UserDefined, null, Const(location), Const(c), ConstF(c == 3 ? 1f : 0f));
+                }
+            }
         }
 
         private static void EmitOps(EmitterContext context, Block block)
diff --git a/Ryujinx.Graphics.Shader/Translation/TranslatorContext.cs b/Ryujinx.Graphics.Shader/Translation/TranslatorContext.cs
index 856b16b7..4a304f3a 100644
--- a/Ryujinx.Graphics.Shader/Translation/TranslatorContext.cs
+++ b/Ryujinx.Graphics.Shader/Translation/TranslatorContext.cs
@@ -34,15 +34,16 @@ namespace Ryujinx.Graphics.Shader.Translation
             _config = config;
         }
 
-        private static bool IsUserAttribute(Operand operand)
+        private static bool IsLoadUserDefined(Operation operation)
         {
-            if (operand != null && operand.Type.IsAttribute())
-            {
-                int value = operand.Value & AttributeConsts.Mask;
-                return value >= AttributeConsts.UserAttributeBase && value < AttributeConsts.UserAttributeEnd;
-            }
+            // TODO: Check if sources count match and all sources are constant.
+            return operation.Inst == Instruction.Load && (IoVariable)operation.GetSource(0).Value == IoVariable.UserDefined;
+        }
 
-            return false;
+        private static bool IsStoreUserDefined(Operation operation)
+        {
+            // TODO: Check if sources count match and all sources are constant.
+            return operation.Inst == Instruction.Store && (IoVariable)operation.GetSource(0).Value == IoVariable.UserDefined;
         }
 
         private static FunctionCode[] Combine(FunctionCode[] a, FunctionCode[] b, int aStart)
@@ -68,9 +69,9 @@ namespace Ryujinx.Graphics.Shader.Translation
             {
                 Operation operation = a[0].Code[index];
 
-                if (IsUserAttribute(operation.Dest))
+                if (IsStoreUserDefined(operation))
                 {
-                    int tIndex = (operation.Dest.Value - AttributeConsts.UserAttributeBase) / 4;
+                    int tIndex = operation.GetSource(1).Value * 4 + operation.GetSource(2).Value;
 
                     Operand temp = temps[tIndex];
 
@@ -82,6 +83,7 @@ namespace Ryujinx.Graphics.Shader.Translation
                     }
 
                     operation.Dest = temp;
+                    operation.TurnIntoCopy(operation.GetSource(operation.SourcesCount - 1));
                 }
 
                 if (operation.Inst == Instruction.Return)
@@ -100,18 +102,15 @@ namespace Ryujinx.Graphics.Shader.Translation
             {
                 Operation operation = b[0].Code[index];
 
-                for (int srcIndex = 0; srcIndex < operation.SourcesCount; srcIndex++)
+                if (IsLoadUserDefined(operation))
                 {
-                    Operand src = operation.GetSource(srcIndex);
+                    int tIndex = operation.GetSource(1).Value * 4 + operation.GetSource(2).Value;
 
-                    if (IsUserAttribute(src))
-                    {
-                        Operand temp = temps[(src.Value - AttributeConsts.UserAttributeBase) / 4];
+                    Operand temp = temps[tIndex];
 
-                        if (temp != null)
-                        {
-                            operation.SetSource(srcIndex, temp);
-                        }
+                    if (temp != null)
+                    {
+                        operation.TurnIntoCopy(temp);
                     }
                 }
 
@@ -209,15 +208,15 @@ namespace Ryujinx.Graphics.Shader.Translation
                     {
                         int attr = AttributeConsts.UserAttributeBase + attrIndex * 16 + c * 4;
 
-                        Operand value = context.LoadAttribute(Const(attr), Const(0), Const(v));
+                        Operand value = context.Load(StorageKind.Input, IoVariable.UserDefined, Const(v), Const(attrIndex), Const(c));
 
                         if (attr == layerOutputAttr)
                         {
-                            context.Copy(Attribute(AttributeConsts.Layer), value);
+                            context.Store(StorageKind.Output, IoVariable.Layer, null, value);
                         }
                         else
                         {
-                            context.Copy(Attribute(attr), value);
+                            context.Store(StorageKind.Output, IoVariable.UserDefined, null, Const(attrIndex), Const(c), value);
                             config.SetOutputUserAttribute(attrIndex);
                         }
 
@@ -227,11 +226,9 @@ namespace Ryujinx.Graphics.Shader.Translation
 
                 for (int c = 0; c < 4; c++)
                 {
-                    int attr = AttributeConsts.PositionX + c * 4;
-
-                    Operand value = context.LoadAttribute(Const(attr), Const(0), Const(v));
+                    Operand value = context.Load(StorageKind.Input, IoVariable.Position, Const(v), Const(c));
 
-                    context.Copy(Attribute(attr), value);
+                    context.Store(StorageKind.Output, IoVariable.Position, null, Const(c), value);
                 }
 
                 context.EmitVertex();
diff --git a/Ryujinx.Graphics.Vulkan/HardwareCapabilities.cs b/Ryujinx.Graphics.Vulkan/HardwareCapabilities.cs
index e206bb29..ab82d7b4 100644
--- a/Ryujinx.Graphics.Vulkan/HardwareCapabilities.cs
+++ b/Ryujinx.Graphics.Vulkan/HardwareCapabilities.cs
@@ -40,6 +40,7 @@ namespace Ryujinx.Graphics.Vulkan
         public readonly bool SupportsPreciseOcclusionQueries;
         public readonly bool SupportsPipelineStatisticsQuery;
         public readonly bool SupportsGeometryShader;
+        public readonly bool SupportsViewportArray2;
         public readonly uint MinSubgroupSize;
         public readonly uint MaxSubgroupSize;
         public readonly ShaderStageFlags RequiredSubgroupSizeStages;
@@ -73,6 +74,7 @@ namespace Ryujinx.Graphics.Vulkan
             bool supportsPreciseOcclusionQueries,
             bool supportsPipelineStatisticsQuery,
             bool supportsGeometryShader,
+            bool supportsViewportArray2,
             uint minSubgroupSize,
             uint maxSubgroupSize,
             ShaderStageFlags requiredSubgroupSizeStages,
@@ -105,6 +107,7 @@ namespace Ryujinx.Graphics.Vulkan
             SupportsPreciseOcclusionQueries = supportsPreciseOcclusionQueries;
             SupportsPipelineStatisticsQuery = supportsPipelineStatisticsQuery;
             SupportsGeometryShader = supportsGeometryShader;
+            SupportsViewportArray2 = supportsViewportArray2;
             MinSubgroupSize = minSubgroupSize;
             MaxSubgroupSize = maxSubgroupSize;
             RequiredSubgroupSizeStages = requiredSubgroupSizeStages;
diff --git a/Ryujinx.Graphics.Vulkan/Shader.cs b/Ryujinx.Graphics.Vulkan/Shader.cs
index 26d0ca40..ca99ebf0 100644
--- a/Ryujinx.Graphics.Vulkan/Shader.cs
+++ b/Ryujinx.Graphics.Vulkan/Shader.cs
@@ -76,10 +76,6 @@ namespace Ryujinx.Graphics.Vulkan
 
         private unsafe static byte[] GlslToSpirv(string glsl, ShaderStage stage)
         {
-            // TODO: We should generate the correct code on the shader translator instead of doing this compensation.
-            glsl = glsl.Replace("gl_VertexID", "(gl_VertexIndex - gl_BaseVertex)");
-            glsl = glsl.Replace("gl_InstanceID", "(gl_InstanceIndex - gl_BaseInstance)");
-
             Options options;
 
             lock (_shaderOptionsLock)
diff --git a/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs b/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs
index 4f69cb1d..50a6fcb9 100644
--- a/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs
+++ b/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs
@@ -39,7 +39,8 @@ namespace Ryujinx.Graphics.Vulkan
             "VK_EXT_shader_subgroup_ballot",
             "VK_EXT_subgroup_size_control",
             "VK_NV_geometry_shader_passthrough",
-            "VK_KHR_portability_subset", // By spec, we should enable this if present.
+            "VK_NV_viewport_array2",
+            "VK_KHR_portability_subset" // As per spec, we should enable this if present.
         };
 
         private static readonly string[] _requiredExtensions = new string[]
diff --git a/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs b/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs
index 1c295d6f..e7475b6b 100644
--- a/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs
+++ b/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs
@@ -306,6 +306,7 @@ namespace Ryujinx.Graphics.Vulkan
                 features2.Features.OcclusionQueryPrecise,
                 _physicalDevice.PhysicalDeviceFeatures.PipelineStatisticsQuery,
                 _physicalDevice.PhysicalDeviceFeatures.GeometryShader,
+                _physicalDevice.IsDeviceExtensionPresent("VK_NV_viewport_array2"),
                 propertiesSubgroupSizeControl.MinSubgroupSize,
                 propertiesSubgroupSizeControl.MaxSubgroupSize,
                 propertiesSubgroupSizeControl.RequiredSubgroupSizeStages,
@@ -568,7 +569,8 @@ namespace Ryujinx.Graphics.Vulkan
                 supportsNonConstantTextureOffset: false,
                 supportsShaderBallot: false,
                 supportsTextureShadowLod: false,
-                supportsViewportIndex: featuresVk12.ShaderOutputViewportIndex,
+                supportsViewportIndexVertexTessellation: featuresVk12.ShaderOutputViewportIndex,
+                supportsViewportMask: Capabilities.SupportsViewportArray2,
                 supportsViewportSwizzle: false,
                 supportsIndirectParameters: true,
                 maximumUniformBuffersPerStage: Constants.MaxUniformBuffersPerStage,
diff --git a/Ryujinx.ShaderTools/Program.cs b/Ryujinx.ShaderTools/Program.cs
index 746b780c..3acebbda 100644
--- a/Ryujinx.ShaderTools/Program.cs
+++ b/Ryujinx.ShaderTools/Program.cs
@@ -86,8 +86,8 @@ namespace Ryujinx.ShaderTools
         static void Main(string[] args)
         {
             Parser.Default.ParseArguments<Options>(args)
-            .WithParsed(options => HandleArguments(options))
-            .WithNotParsed(errors => errors.Output());
+                .WithParsed(options => HandleArguments(options))
+                .WithNotParsed(errors => errors.Output());
         }
     }
 }
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2