From d512ce122cb1c9a7fe7cb40d3f85d642ee37f897 Mon Sep 17 00:00:00 2001
From: gdkchan <gab.dark.100@gmail.com>
Date: Mon, 18 Oct 2021 18:38:04 -0300
Subject: [PATCH] Initial tessellation shader support (#2534)

* Initial tessellation shader support

* Nits

* Re-arrange built-in table

* This is not needed anymore

* PR feedback
---
 Ryujinx.Graphics.GAL/IPipeline.cs             |   6 +-
 .../Multithreading/CommandHelper.cs           |   4 +
 .../Multithreading/CommandType.cs             |   2 +
 .../Commands/SetPatchParametersCommand.cs     |  25 ++++
 .../Commands/SetPolygonModeCommand.cs         |  20 +++
 .../Multithreading/ThreadedPipeline.cs        |  12 ++
 Ryujinx.Graphics.GAL/PolygonMode.cs           |   9 ++
 .../Engine/Compute/ComputeClass.cs            |   3 +-
 .../Engine/Threed/StateUpdater.cs             |  31 ++++-
 .../Engine/Threed/ThreedClassState.cs         |  53 +++++++-
 .../Shader/Cache/CacheHelper.cs               |  21 +++
 .../Definition/GuestGpuAccessorHeader.cs      |   7 +-
 .../Shader/CachedGpuAccessor.cs               |  27 ++++
 Ryujinx.Graphics.Gpu/Shader/GpuAccessor.cs    |  23 +++-
 .../Shader/GpuAccessorState.cs                |  11 +-
 Ryujinx.Graphics.Gpu/Shader/ShaderCache.cs    |   2 +-
 Ryujinx.Graphics.OpenGL/EnumConversion.cs     |  17 +++
 Ryujinx.Graphics.OpenGL/Pipeline.cs           |  28 ++++
 .../CodeGen/Glsl/Declarations.cs              |  73 +++++++++-
 .../CodeGen/Glsl/DefaultNames.cs              |   1 +
 .../CodeGen/Glsl/GlslGenerator.cs             |   5 +-
 .../Glsl/Instructions/InstGenMemory.cs        |   6 +-
 .../CodeGen/Glsl/OperandManager.cs            | 125 ++++++++++++------
 Ryujinx.Graphics.Shader/Decoders/Decoder.cs   |  10 +-
 .../Decoders/InstDecoders.cs                  |   3 +-
 Ryujinx.Graphics.Shader/IGpuAccessor.cs       |  15 +++
 .../Instructions/InstEmit.cs                  |   7 -
 .../Instructions/InstEmitAttribute.cs         |  32 ++++-
 .../Instructions/InstEmitMove.cs              |  20 +++
 .../Instructions/InstEmitVideoMinMax.cs       |  62 +++++++++
 .../IntermediateRepresentation/Instruction.cs |   6 +
 .../OperandHelper.cs                          |   5 +
 .../IntermediateRepresentation/OperandType.cs |   9 ++
 .../StructuredIr/OperandInfo.cs               |  16 +--
 .../StructuredIr/StructuredProgramContext.cs  |   6 +
 Ryujinx.Graphics.Shader/TessPatchType.cs      |  22 +++
 Ryujinx.Graphics.Shader/TessSpacing.cs        |  22 +++
 .../Translation/AttributeConsts.cs            |  69 ++++++----
 .../Translation/EmitterContext.cs             |   2 +-
 .../Translation/ShaderConfig.cs               |  50 ++++---
 .../Translation/Translator.cs                 |  39 ++++--
 .../Translation/TranslatorContext.cs          |  17 ++-
 42 files changed, 775 insertions(+), 148 deletions(-)
 create mode 100644 Ryujinx.Graphics.GAL/Multithreading/Commands/SetPatchParametersCommand.cs
 create mode 100644 Ryujinx.Graphics.GAL/Multithreading/Commands/SetPolygonModeCommand.cs
 create mode 100644 Ryujinx.Graphics.GAL/PolygonMode.cs
 create mode 100644 Ryujinx.Graphics.Shader/TessPatchType.cs
 create mode 100644 Ryujinx.Graphics.Shader/TessSpacing.cs

diff --git a/Ryujinx.Graphics.GAL/IPipeline.cs b/Ryujinx.Graphics.GAL/IPipeline.cs
index 7a7d83cc98..bfc432b1cf 100644
--- a/Ryujinx.Graphics.GAL/IPipeline.cs
+++ b/Ryujinx.Graphics.GAL/IPipeline.cs
@@ -55,11 +55,15 @@ namespace Ryujinx.Graphics.GAL
 
         void SetImage(int binding, ITexture texture, Format imageFormat);
 
+        void SetLineParameters(float width, bool smooth);
+
         void SetLogicOpState(bool enable, LogicalOp op);
 
-        void SetLineParameters(float width, bool smooth);
+        void SetPatchParameters(int vertices, ReadOnlySpan<float> defaultOuterLevel, ReadOnlySpan<float> defaultInnerLevel);
         void SetPointParameters(float size, bool isProgramPointSize, bool enablePointSprite, Origin origin);
 
+        void SetPolygonMode(PolygonMode frontMode, PolygonMode backMode);
+
         void SetPrimitiveRestart(bool enable, int index);
 
         void SetPrimitiveTopology(PrimitiveTopology topology);
diff --git a/Ryujinx.Graphics.GAL/Multithreading/CommandHelper.cs b/Ryujinx.Graphics.GAL/Multithreading/CommandHelper.cs
index 82a75ea7c6..47ceeb7d55 100644
--- a/Ryujinx.Graphics.GAL/Multithreading/CommandHelper.cs
+++ b/Ryujinx.Graphics.GAL/Multithreading/CommandHelper.cs
@@ -181,8 +181,12 @@ namespace Ryujinx.Graphics.GAL.Multithreading
                 SetLineParametersCommand.Run(ref GetCommand<SetLineParametersCommand>(memory), threaded, renderer);
             _lookup[(int)CommandType.SetLogicOpState] = (Span<byte> memory, ThreadedRenderer threaded, IRenderer renderer) =>
                 SetLogicOpStateCommand.Run(ref GetCommand<SetLogicOpStateCommand>(memory), threaded, renderer);
+            _lookup[(int)CommandType.SetPatchParameters] = (Span<byte> memory, ThreadedRenderer threaded, IRenderer renderer) =>
+                SetPatchParametersCommand.Run(ref GetCommand<SetPatchParametersCommand>(memory), threaded, renderer);
             _lookup[(int)CommandType.SetPointParameters] = (Span<byte> memory, ThreadedRenderer threaded, IRenderer renderer) =>
                 SetPointParametersCommand.Run(ref GetCommand<SetPointParametersCommand>(memory), threaded, renderer);
+            _lookup[(int)CommandType.SetPolygonMode] = (Span<byte> memory, ThreadedRenderer threaded, IRenderer renderer) =>
+                SetPolygonModeCommand.Run(ref GetCommand<SetPolygonModeCommand>(memory), threaded, renderer);
             _lookup[(int)CommandType.SetPrimitiveRestart] = (Span<byte> memory, ThreadedRenderer threaded, IRenderer renderer) =>
                 SetPrimitiveRestartCommand.Run(ref GetCommand<SetPrimitiveRestartCommand>(memory), threaded, renderer);
             _lookup[(int)CommandType.SetPrimitiveTopology] = (Span<byte> memory, ThreadedRenderer threaded, IRenderer renderer) =>
diff --git a/Ryujinx.Graphics.GAL/Multithreading/CommandType.cs b/Ryujinx.Graphics.GAL/Multithreading/CommandType.cs
index 0761a7f08d..ac73a3febe 100644
--- a/Ryujinx.Graphics.GAL/Multithreading/CommandType.cs
+++ b/Ryujinx.Graphics.GAL/Multithreading/CommandType.cs
@@ -72,7 +72,9 @@
         SetIndexBuffer,
         SetLineParameters,
         SetLogicOpState,
+        SetPatchParameters,
         SetPointParameters,
+        SetPolygonMode,
         SetPrimitiveRestart,
         SetPrimitiveTopology,
         SetProgram,
diff --git a/Ryujinx.Graphics.GAL/Multithreading/Commands/SetPatchParametersCommand.cs b/Ryujinx.Graphics.GAL/Multithreading/Commands/SetPatchParametersCommand.cs
new file mode 100644
index 0000000000..7847e8d083
--- /dev/null
+++ b/Ryujinx.Graphics.GAL/Multithreading/Commands/SetPatchParametersCommand.cs
@@ -0,0 +1,25 @@
+using Ryujinx.Common.Memory;
+using System;
+
+namespace Ryujinx.Graphics.GAL.Multithreading.Commands
+{
+    struct SetPatchParametersCommand : IGALCommand
+    {
+        public CommandType CommandType => CommandType.SetPatchParameters;
+        private int _vertices;
+        private Array4<float> _defaultOuterLevel;
+        private Array2<float> _defaultInnerLevel;
+
+        public void Set(int vertices, ReadOnlySpan<float> defaultOuterLevel, ReadOnlySpan<float> defaultInnerLevel)
+        {
+            _vertices = vertices;
+            defaultOuterLevel.CopyTo(_defaultOuterLevel.ToSpan());
+            defaultInnerLevel.CopyTo(_defaultInnerLevel.ToSpan());
+        }
+
+        public static void Run(ref SetPatchParametersCommand command, ThreadedRenderer threaded, IRenderer renderer)
+        {
+            renderer.Pipeline.SetPatchParameters(command._vertices, command._defaultOuterLevel.ToSpan(), command._defaultInnerLevel.ToSpan());
+        }
+    }
+}
diff --git a/Ryujinx.Graphics.GAL/Multithreading/Commands/SetPolygonModeCommand.cs b/Ryujinx.Graphics.GAL/Multithreading/Commands/SetPolygonModeCommand.cs
new file mode 100644
index 0000000000..6de78f045f
--- /dev/null
+++ b/Ryujinx.Graphics.GAL/Multithreading/Commands/SetPolygonModeCommand.cs
@@ -0,0 +1,20 @@
+namespace Ryujinx.Graphics.GAL.Multithreading.Commands
+{
+    struct SetPolygonModeCommand : IGALCommand
+    {
+        public CommandType CommandType => CommandType.SetPolygonMode;
+        private PolygonMode _frontMode;
+        private PolygonMode _backMode;
+
+        public void Set(PolygonMode frontMode, PolygonMode backMode)
+        {
+            _frontMode = frontMode;
+            _backMode = backMode;
+        }
+
+        public static void Run(ref SetPolygonModeCommand command, ThreadedRenderer threaded, IRenderer renderer)
+        {
+            renderer.Pipeline.SetPolygonMode(command._frontMode, command._backMode);
+        }
+    }
+}
diff --git a/Ryujinx.Graphics.GAL/Multithreading/ThreadedPipeline.cs b/Ryujinx.Graphics.GAL/Multithreading/ThreadedPipeline.cs
index 0f52348170..3c39a77f94 100644
--- a/Ryujinx.Graphics.GAL/Multithreading/ThreadedPipeline.cs
+++ b/Ryujinx.Graphics.GAL/Multithreading/ThreadedPipeline.cs
@@ -179,12 +179,24 @@ namespace Ryujinx.Graphics.GAL.Multithreading
             _renderer.QueueCommand();
         }
 
+        public void SetPatchParameters(int vertices, ReadOnlySpan<float> defaultOuterLevel, ReadOnlySpan<float> defaultInnerLevel)
+        {
+            _renderer.New<SetPatchParametersCommand>().Set(vertices, defaultOuterLevel, defaultInnerLevel);
+            _renderer.QueueCommand();
+        }
+
         public void SetPointParameters(float size, bool isProgramPointSize, bool enablePointSprite, Origin origin)
         {
             _renderer.New<SetPointParametersCommand>().Set(size, isProgramPointSize, enablePointSprite, origin);
             _renderer.QueueCommand();
         }
 
+        public void SetPolygonMode(PolygonMode frontMode, PolygonMode backMode)
+        {
+            _renderer.New<SetPolygonModeCommand>().Set(frontMode, backMode);
+            _renderer.QueueCommand();
+        }
+
         public void SetPrimitiveRestart(bool enable, int index)
         {
             _renderer.New<SetPrimitiveRestartCommand>().Set(enable, index);
diff --git a/Ryujinx.Graphics.GAL/PolygonMode.cs b/Ryujinx.Graphics.GAL/PolygonMode.cs
new file mode 100644
index 0000000000..d6110c1b33
--- /dev/null
+++ b/Ryujinx.Graphics.GAL/PolygonMode.cs
@@ -0,0 +1,9 @@
+namespace Ryujinx.Graphics.GAL
+{
+    public enum PolygonMode
+    {
+        Point = 0x1b00,
+        Line = 0x1b01,
+        Fill = 0x1b02
+    }
+}
diff --git a/Ryujinx.Graphics.Gpu/Engine/Compute/ComputeClass.cs b/Ryujinx.Graphics.Gpu/Engine/Compute/ComputeClass.cs
index 8469f1aed0..00015c4046 100644
--- a/Ryujinx.Graphics.Gpu/Engine/Compute/ComputeClass.cs
+++ b/Ryujinx.Graphics.Gpu/Engine/Compute/ComputeClass.cs
@@ -129,7 +129,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Compute
                 _state.State.SetTexHeaderPoolCMaximumIndex,
                 _state.State.SetBindlessTextureConstantBufferSlotSelect,
                 false,
-                PrimitiveTopology.Points);
+                PrimitiveTopology.Points,
+                default);
 
             ShaderBundle cs = memoryManager.Physical.ShaderCache.GetComputeShader(
                 _channel,
diff --git a/Ryujinx.Graphics.Gpu/Engine/Threed/StateUpdater.cs b/Ryujinx.Graphics.Gpu/Engine/Threed/StateUpdater.cs
index f9d16803e2..4a5633c9c6 100644
--- a/Ryujinx.Graphics.Gpu/Engine/Threed/StateUpdater.cs
+++ b/Ryujinx.Graphics.Gpu/Engine/Threed/StateUpdater.cs
@@ -72,6 +72,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
                     nameof(ThreedClassState.VertexBufferState),
                     nameof(ThreedClassState.VertexBufferEndAddress)),
 
+                new StateUpdateCallbackEntry(UpdateTessellationState,
+                    nameof(ThreedClassState.TessOuterLevel),
+                    nameof(ThreedClassState.TessInnerLevel),
+                    nameof(ThreedClassState.PatchVertices)),
+
                 new StateUpdateCallbackEntry(UpdateTfBufferState, nameof(ThreedClassState.TfBufferState)),
                 new StateUpdateCallbackEntry(UpdateUserClipState, nameof(ThreedClassState.ClipDistanceEnable)),
 
@@ -100,6 +105,10 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
                     nameof(ThreedClassState.ViewportExtents),
                     nameof(ThreedClassState.YControl)),
 
+                new StateUpdateCallbackEntry(UpdatePolygonMode,
+                    nameof(ThreedClassState.PolygonModeFront),
+                    nameof(ThreedClassState.PolygonModeBack)),
+
                 new StateUpdateCallbackEntry(UpdateDepthBiasState,
                     nameof(ThreedClassState.DepthBiasState),
                     nameof(ThreedClassState.DepthBiasFactor),
@@ -259,6 +268,17 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
             }
         }
 
+        /// <summary>
+        /// Updates tessellation state based on the guest GPU state.
+        /// </summary>
+        private void UpdateTessellationState()
+        {
+            _context.Renderer.Pipeline.SetPatchParameters(
+                _state.State.PatchVertices,
+                _state.State.TessOuterLevel.ToSpan(),
+                _state.State.TessInnerLevel.ToSpan());
+        }
+
         /// <summary>
         /// Updates transform feedback buffer state based on the guest GPU state.
         /// </summary>
@@ -544,6 +564,14 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
             _context.Renderer.Pipeline.SetViewports(0, viewports);
         }
 
+        /// <summary>
+        /// Updates polygon mode state based on current GPU state.
+        /// </summary>
+        private void UpdatePolygonMode()
+        {
+            _context.Renderer.Pipeline.SetPolygonMode(_state.State.PolygonModeFront, _state.State.PolygonModeBack);
+        }
+
         /// <summary>
         /// Updates host depth bias (also called polygon offset) state based on current GPU state.
         /// </summary>
@@ -949,7 +977,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
                 _state.State.TexturePoolState.MaximumId,
                 (int)_state.State.TextureBufferIndex,
                 _state.State.EarlyZForce,
-                _drawState.Topology);
+                _drawState.Topology,
+                _state.State.TessMode);
 
             ShaderBundle gs = _channel.MemoryManager.Physical.ShaderCache.GetGraphicsShader(ref _state.State, _channel, gas, addresses);
 
diff --git a/Ryujinx.Graphics.Gpu/Engine/Threed/ThreedClassState.cs b/Ryujinx.Graphics.Gpu/Engine/Threed/ThreedClassState.cs
index a6392e3d70..58bc0957fb 100644
--- a/Ryujinx.Graphics.Gpu/Engine/Threed/ThreedClassState.cs
+++ b/Ryujinx.Graphics.Gpu/Engine/Threed/ThreedClassState.cs
@@ -3,6 +3,7 @@ using Ryujinx.Graphics.GAL;
 using Ryujinx.Graphics.Gpu.Engine.InlineToMemory;
 using Ryujinx.Graphics.Gpu.Engine.Types;
 using Ryujinx.Graphics.Gpu.Image;
+using Ryujinx.Graphics.Shader;
 using System;
 
 namespace Ryujinx.Graphics.Gpu.Engine.Threed
@@ -19,6 +20,43 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
         Fragment
     }
 
+    /// <summary>
+    /// Tessellation mode.
+    /// </summary>
+    struct TessMode
+    {
+#pragma warning disable CS0649
+        public uint Packed;
+#pragma warning restore CS0649
+
+        /// <summary>
+        /// Unpacks the tessellation abstract patch type.
+        /// </summary>
+        /// <returns>Abtract patch type</returns>
+        public TessPatchType UnpackPatchType()
+        {
+            return (TessPatchType)(Packed & 3);
+        }
+
+        /// <summary>
+        /// Unpacks the spacing between tessellated vertices of the patch.
+        /// </summary>
+        /// <returns>Spacing between tessellated vertices</returns>
+        public TessSpacing UnpackSpacing()
+        {
+            return (TessSpacing)((Packed >> 4) & 3);
+        }
+
+        /// <summary>
+        /// Unpacks the primitive winding order.
+        /// </summary>
+        /// <returns>True if clockwise, false if counter-clockwise</returns>
+        public bool UnpackCw()
+        {
+            return (Packed & (1 << 8)) != 0;
+        }
+    }
+
     /// <summary>
     /// Transform feedback buffer state.
     /// </summary>
@@ -661,7 +699,11 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
         public Boolean32 EarlyZForce;
         public fixed uint Reserved214[45];
         public uint SyncpointAction;
-        public fixed uint Reserved2CC[44];
+        public fixed uint Reserved2CC[21];
+        public TessMode TessMode;
+        public Array4<float> TessOuterLevel;
+        public Array2<float> TessInnerLevel;
+        public fixed uint Reserved33C[16];
         public Boolean32 RasterizeEnable;
         public Array4<TfBufferState> TfBufferState;
         public fixed uint Reserved400[192];
@@ -679,9 +721,14 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
         public float ClearDepthValue;
         public fixed uint ReservedD94[3];
         public uint ClearStencilValue;
-        public fixed uint ReservedDA4[7];
+        public fixed uint ReservedDA4[2];
+        public PolygonMode PolygonModeFront;
+        public PolygonMode PolygonModeBack;
+        public Boolean32 PolygonSmoothEnable;
+        public fixed uint ReservedDB8[2];
         public DepthBiasState DepthBiasState;
-        public fixed uint ReservedDCC[5];
+        public int PatchVertices;
+        public fixed uint ReservedDD0[4];
         public uint TextureBarrier;
         public fixed uint ReservedDE4[7];
         public Array16<ScissorState> ScissorState;
diff --git a/Ryujinx.Graphics.Gpu/Shader/Cache/CacheHelper.cs b/Ryujinx.Graphics.Gpu/Shader/Cache/CacheHelper.cs
index 33da42db0b..09107346e0 100644
--- a/Ryujinx.Graphics.Gpu/Shader/Cache/CacheHelper.cs
+++ b/Ryujinx.Graphics.Gpu/Shader/Cache/CacheHelper.cs
@@ -349,6 +349,26 @@ namespace Ryujinx.Graphics.Gpu.Shader.Cache
             return flags;
         }
 
+        /// <summary>
+        /// Packs the tessellation parameters from the gpu accessor.
+        /// </summary>
+        /// <param name="gpuAccessor">The gpu accessor</param>
+        /// <returns>The packed tessellation parameters</returns>
+        private static byte GetTessellationModePacked(IGpuAccessor gpuAccessor)
+        {
+            byte value;
+
+            value = (byte)((int)gpuAccessor.QueryTessPatchType() & 3);
+            value |= (byte)(((int)gpuAccessor.QueryTessSpacing() & 3) << 2);
+
+            if (gpuAccessor.QueryTessCw())
+            {
+                value |= 0x10;
+            }
+
+            return value;
+        }
+
         /// <summary>
         /// Create a new instance of <see cref="GuestGpuAccessorHeader"/> from an gpu accessor.
         /// </summary>
@@ -364,6 +384,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.Cache
                 ComputeLocalMemorySize = gpuAccessor.QueryComputeLocalMemorySize(),
                 ComputeSharedMemorySize = gpuAccessor.QueryComputeSharedMemorySize(),
                 PrimitiveTopology = gpuAccessor.QueryPrimitiveTopology(),
+                TessellationModePacked = GetTessellationModePacked(gpuAccessor),
                 StateFlags = GetGpuStateFlags(gpuAccessor)
             };
         }
diff --git a/Ryujinx.Graphics.Gpu/Shader/Cache/Definition/GuestGpuAccessorHeader.cs b/Ryujinx.Graphics.Gpu/Shader/Cache/Definition/GuestGpuAccessorHeader.cs
index 610b2da10e..2e044750ef 100644
--- a/Ryujinx.Graphics.Gpu/Shader/Cache/Definition/GuestGpuAccessorHeader.cs
+++ b/Ryujinx.Graphics.Gpu/Shader/Cache/Definition/GuestGpuAccessorHeader.cs
@@ -49,10 +49,15 @@ namespace Ryujinx.Graphics.Gpu.Shader.Cache.Definition
         /// </summary>
         public InputTopology PrimitiveTopology;
 
+        /// <summary>
+        /// Tessellation parameters (packed to fit on a byte).
+        /// </summary>
+        public byte TessellationModePacked;
+
         /// <summary>
         /// Unused/reserved.
         /// </summary>
-        public ushort Reserved2;
+        public byte Reserved2;
 
         /// <summary>
         /// GPU boolean state that can influence shader compilation.
diff --git a/Ryujinx.Graphics.Gpu/Shader/CachedGpuAccessor.cs b/Ryujinx.Graphics.Gpu/Shader/CachedGpuAccessor.cs
index 3a52b2feb1..21d08823e7 100644
--- a/Ryujinx.Graphics.Gpu/Shader/CachedGpuAccessor.cs
+++ b/Ryujinx.Graphics.Gpu/Shader/CachedGpuAccessor.cs
@@ -134,6 +134,33 @@ namespace Ryujinx.Graphics.Gpu.Shader
             return _header.PrimitiveTopology;
         }
 
+        /// <summary>
+        /// Queries the tessellation evaluation shader primitive winding order.
+        /// </summary>
+        /// <returns>True if the primitive winding order is clockwise, false if counter-clockwise</returns>
+        public bool QueryTessCw()
+        {
+            return (_header.TessellationModePacked & 0x10) != 0;
+        }
+
+        /// <summary>
+        /// Queries the tessellation evaluation shader abstract patch type.
+        /// </summary>
+        /// <returns>Abstract patch type</returns>
+        public TessPatchType QueryTessPatchType()
+        {
+            return (TessPatchType)(_header.TessellationModePacked & 3);
+        }
+
+        /// <summary>
+        /// Queries the tessellation evaluation shader spacing between tessellated vertices of the patch.
+        /// </summary>
+        /// <returns>Spacing between tessellated vertices of the patch</returns>
+        public TessSpacing QueryTessSpacing()
+        {
+            return (TessSpacing)((_header.TessellationModePacked >> 2) & 3);
+        }
+
         /// <summary>
         /// Gets the texture descriptor for a given texture on the pool.
         /// </summary>
diff --git a/Ryujinx.Graphics.Gpu/Shader/GpuAccessor.cs b/Ryujinx.Graphics.Gpu/Shader/GpuAccessor.cs
index 50e24b97db..64604a99c9 100644
--- a/Ryujinx.Graphics.Gpu/Shader/GpuAccessor.cs
+++ b/Ryujinx.Graphics.Gpu/Shader/GpuAccessor.cs
@@ -168,10 +168,31 @@ namespace Ryujinx.Graphics.Gpu.Shader
                 PrimitiveTopology.TriangleFan => InputTopology.Triangles,
                 PrimitiveTopology.TrianglesAdjacency or
                 PrimitiveTopology.TriangleStripAdjacency => InputTopology.TrianglesAdjacency,
-                _ => InputTopology.Points,
+                PrimitiveTopology.Patches => _state.TessellationMode.UnpackPatchType() == TessPatchType.Isolines
+                    ? InputTopology.Lines
+                    : InputTopology.Triangles,
+                _ => InputTopology.Points
             };
         }
 
+        /// <summary>
+        /// Queries the tessellation evaluation shader primitive winding order.
+        /// </summary>
+        /// <returns>True if the primitive winding order is clockwise, false if counter-clockwise</returns>
+        public bool QueryTessCw() => _state.TessellationMode.UnpackCw();
+
+        /// <summary>
+        /// Queries the tessellation evaluation shader abstract patch type.
+        /// </summary>
+        /// <returns>Abstract patch type</returns>
+        public TessPatchType QueryTessPatchType() => _state.TessellationMode.UnpackPatchType();
+
+        /// <summary>
+        /// Queries the tessellation evaluation shader spacing between tessellated vertices of the patch.
+        /// </summary>
+        /// <returns>Spacing between tessellated vertices of the patch</returns>
+        public TessSpacing QueryTessSpacing() => _state.TessellationMode.UnpackSpacing();
+
         /// <summary>
         /// Gets the texture descriptor for a given texture on the pool.
         /// </summary>
diff --git a/Ryujinx.Graphics.Gpu/Shader/GpuAccessorState.cs b/Ryujinx.Graphics.Gpu/Shader/GpuAccessorState.cs
index 8d81711321..ebbf3b696c 100644
--- a/Ryujinx.Graphics.Gpu/Shader/GpuAccessorState.cs
+++ b/Ryujinx.Graphics.Gpu/Shader/GpuAccessorState.cs
@@ -1,4 +1,5 @@
 using Ryujinx.Graphics.GAL;
+using Ryujinx.Graphics.Gpu.Engine.Threed;
 
 namespace Ryujinx.Graphics.Gpu.Shader
 {
@@ -32,6 +33,11 @@ namespace Ryujinx.Graphics.Gpu.Shader
         /// </summary>
         public PrimitiveTopology Topology { get; }
 
+        /// <summary>
+        /// Tessellation mode.
+        /// </summary>
+        public TessMode TessellationMode { get; }
+
         /// <summary>
         /// Creates a new instance of the GPU accessor state.
         /// </summary>
@@ -40,18 +46,21 @@ namespace Ryujinx.Graphics.Gpu.Shader
         /// <param name="textureBufferIndex">Constant buffer slot where the texture handles are located</param>
         /// <param name="earlyZForce">Early Z force enable</param>
         /// <param name="topology">Primitive topology</param>
+        /// <param name="tessellationMode">Tessellation mode</param>
         public GpuAccessorState(
             ulong texturePoolGpuVa,
             int texturePoolMaximumId,
             int textureBufferIndex,
             bool earlyZForce,
-            PrimitiveTopology topology)
+            PrimitiveTopology topology,
+            TessMode tessellationMode)
         {
             TexturePoolGpuVa = texturePoolGpuVa;
             TexturePoolMaximumId = texturePoolMaximumId;
             TextureBufferIndex = textureBufferIndex;
             EarlyZForce = earlyZForce;
             Topology = topology;
+            TessellationMode = tessellationMode;
         }
     }
 }
\ No newline at end of file
diff --git a/Ryujinx.Graphics.Gpu/Shader/ShaderCache.cs b/Ryujinx.Graphics.Gpu/Shader/ShaderCache.cs
index e69e7dcbe9..f2180820d5 100644
--- a/Ryujinx.Graphics.Gpu/Shader/ShaderCache.cs
+++ b/Ryujinx.Graphics.Gpu/Shader/ShaderCache.cs
@@ -40,7 +40,7 @@ namespace Ryujinx.Graphics.Gpu.Shader
         /// <summary>
         /// Version of the codegen (to be changed when codegen or guest format change).
         /// </summary>
-        private const ulong ShaderCodeGenVersion = 2702;
+        private const ulong ShaderCodeGenVersion = 2534;
 
         // Progress reporting helpers
         private volatile int _shaderCount;
diff --git a/Ryujinx.Graphics.OpenGL/EnumConversion.cs b/Ryujinx.Graphics.OpenGL/EnumConversion.cs
index cc3db003b0..ccdbcfece2 100644
--- a/Ryujinx.Graphics.OpenGL/EnumConversion.cs
+++ b/Ryujinx.Graphics.OpenGL/EnumConversion.cs
@@ -290,6 +290,23 @@ namespace Ryujinx.Graphics.OpenGL
             return TextureMinFilter.Nearest;
         }
 
+        public static OpenTK.Graphics.OpenGL.PolygonMode Convert(this GAL.PolygonMode mode)
+        {
+            switch (mode)
+            {
+                case GAL.PolygonMode.Point:
+                    return OpenTK.Graphics.OpenGL.PolygonMode.Point;
+                case GAL.PolygonMode.Line:
+                    return OpenTK.Graphics.OpenGL.PolygonMode.Line;
+                case GAL.PolygonMode.Fill:
+                    return OpenTK.Graphics.OpenGL.PolygonMode.Fill;
+            }
+
+            Logger.Debug?.Print(LogClass.Gpu, $"Invalid {nameof(GAL.PolygonMode)} enum value: {mode}.");
+
+            return OpenTK.Graphics.OpenGL.PolygonMode.Fill;
+        }
+
         public static PrimitiveType Convert(this PrimitiveTopology topology)
         {
             switch (topology)
diff --git a/Ryujinx.Graphics.OpenGL/Pipeline.cs b/Ryujinx.Graphics.OpenGL/Pipeline.cs
index dd07afcf45..d0a509b474 100644
--- a/Ryujinx.Graphics.OpenGL/Pipeline.cs
+++ b/Ryujinx.Graphics.OpenGL/Pipeline.cs
@@ -830,6 +830,21 @@ namespace Ryujinx.Graphics.OpenGL
             GL.LineWidth(width);
         }
 
+        public unsafe void SetPatchParameters(int vertices, ReadOnlySpan<float> defaultOuterLevel, ReadOnlySpan<float> defaultInnerLevel)
+        {
+            GL.PatchParameter(PatchParameterInt.PatchVertices, vertices);
+
+            fixed (float* pOuterLevel = defaultOuterLevel)
+            {
+                GL.PatchParameter(PatchParameterFloat.PatchDefaultOuterLevel, pOuterLevel);
+            }
+
+            fixed (float* pInnerLevel = defaultInnerLevel)
+            {
+                GL.PatchParameter(PatchParameterFloat.PatchDefaultInnerLevel, pInnerLevel);
+            }
+        }
+
         public void SetPointParameters(float size, bool isProgramPointSize, bool enablePointSprite, Origin origin)
         {
             // GL_POINT_SPRITE was deprecated in core profile 3.2+ and causes GL_INVALID_ENUM when set.
@@ -861,6 +876,19 @@ namespace Ryujinx.Graphics.OpenGL
             GL.PointSize(Math.Max(float.Epsilon, size));
         }
 
+        public void SetPolygonMode(GAL.PolygonMode frontMode, GAL.PolygonMode backMode)
+        {
+            if (frontMode == backMode)
+            {
+                GL.PolygonMode(MaterialFace.FrontAndBack, frontMode.Convert());
+            }
+            else
+            {
+                GL.PolygonMode(MaterialFace.Front, frontMode.Convert());
+                GL.PolygonMode(MaterialFace.Back, backMode.Convert());
+            }
+        }
+
         public void SetPrimitiveRestart(bool enable, int index)
         {
             if (!enable)
diff --git a/Ryujinx.Graphics.Shader/CodeGen/Glsl/Declarations.cs b/Ryujinx.Graphics.Shader/CodeGen/Glsl/Declarations.cs
index e29ff486c8..3e2e51b02a 100644
--- a/Ryujinx.Graphics.Shader/CodeGen/Glsl/Declarations.cs
+++ b/Ryujinx.Graphics.Shader/CodeGen/Glsl/Declarations.cs
@@ -136,6 +136,22 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
 
                     context.AppendLine();
                 }
+                else if (context.Config.Stage == ShaderStage.TessellationControl)
+                {
+                    int threadsPerInputPrimitive = context.Config.ThreadsPerInputPrimitive;
+
+                    context.AppendLine($"layout (vertices = {threadsPerInputPrimitive}) out;");
+                    context.AppendLine();
+                }
+                else if (context.Config.Stage == ShaderStage.TessellationEvaluation)
+                {
+                    string patchType = context.Config.GpuAccessor.QueryTessPatchType().ToGlsl();
+                    string spacing = context.Config.GpuAccessor.QueryTessSpacing().ToGlsl();
+                    string windingOrder = context.Config.GpuAccessor.QueryTessCw() ? "cw" : "ccw";
+
+                    context.AppendLine($"layout ({patchType}, {spacing}, {windingOrder}) in;");
+                    context.AppendLine();
+                }
 
                 if (context.Config.UsedInputAttributes != 0 || context.Config.GpPassthrough)
                 {
@@ -150,6 +166,20 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
 
                     context.AppendLine();
                 }
+
+                if (context.Config.UsedInputAttributesPerPatch != 0)
+                {
+                    DeclareInputAttributesPerPatch(context, context.Config.UsedInputAttributesPerPatch);
+
+                    context.AppendLine();
+                }
+
+                if (context.Config.UsedOutputAttributesPerPatch != 0)
+                {
+                    DeclareUsedOutputAttributesPerPatch(context, context.Config.UsedOutputAttributesPerPatch);
+
+                    context.AppendLine();
+                }
             }
             else
             {
@@ -424,17 +454,25 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
                 while (usedAttributes != 0)
                 {
                     int index = BitOperations.TrailingZeroCount(usedAttributes);
-
                     DeclareInputAttribute(context, info, index);
-
                     usedAttributes &= ~(1 << index);
                 }
             }
         }
 
+        private static void DeclareInputAttributesPerPatch(CodeGenContext context, int usedAttributes)
+        {
+            while (usedAttributes != 0)
+            {
+                int index = BitOperations.TrailingZeroCount(usedAttributes);
+                DeclareInputAttributePerPatch(context, index);
+                usedAttributes &= ~(1 << index);
+            }
+        }
+
         private static void DeclareInputAttribute(CodeGenContext context, StructuredProgramInfo info, int attr)
         {
-            string suffix = context.Config.Stage == ShaderStage.Geometry ? "[]" : string.Empty;
+            string suffix = OperandManager.IsArrayAttribute(context.Config.Stage, isOutAttr: false) ? "[]" : string.Empty;
             string iq = string.Empty;
 
             if (context.Config.Stage == ShaderStage.Fragment)
@@ -465,6 +503,13 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
             }
         }
 
+        private static void DeclareInputAttributePerPatch(CodeGenContext context, int attr)
+        {
+            string name = $"{DefaultNames.PerPatchAttributePrefix}{attr}";
+
+            context.AppendLine($"patch in vec4 {name};");
+        }
+
         private static void DeclareOutputAttributes(CodeGenContext context, StructuredProgramInfo info)
         {
             if (context.Config.UsedFeatures.HasFlag(FeatureFlags.OaIndexing))
@@ -477,9 +522,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
                 while (usedAttributes != 0)
                 {
                     int index = BitOperations.TrailingZeroCount(usedAttributes);
-
                     DeclareOutputAttribute(context, index);
-
                     usedAttributes &= ~(1 << index);
                 }
             }
@@ -487,7 +530,8 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
 
         private static void DeclareOutputAttribute(CodeGenContext context, int attr)
         {
-            string name = $"{DefaultNames.OAttributePrefix}{attr}";
+            string suffix = OperandManager.IsArrayAttribute(context.Config.Stage, isOutAttr: true) ? "[]" : string.Empty;
+            string name = $"{DefaultNames.OAttributePrefix}{attr}{suffix}";
 
             if ((context.Config.Options.Flags & TranslationFlags.Feedback) != 0)
             {
@@ -504,6 +548,23 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
             }
         }
 
+        private static void DeclareUsedOutputAttributesPerPatch(CodeGenContext context, int usedAttributes)
+        {
+            while (usedAttributes != 0)
+            {
+                int index = BitOperations.TrailingZeroCount(usedAttributes);
+                DeclareOutputAttributePerPatch(context, index);
+                usedAttributes &= ~(1 << index);
+            }
+        }
+
+        private static void DeclareOutputAttributePerPatch(CodeGenContext context, int attr)
+        {
+            string name = $"{DefaultNames.PerPatchAttributePrefix}{attr}";
+
+            context.AppendLine($"patch out vec4 {name};");
+        }
+
         private static void DeclareSupportUniformBlock(CodeGenContext context, bool isFragment, int scaleElements)
         {
             if (!isFragment && scaleElements == 0)
diff --git a/Ryujinx.Graphics.Shader/CodeGen/Glsl/DefaultNames.cs b/Ryujinx.Graphics.Shader/CodeGen/Glsl/DefaultNames.cs
index eaf1050c19..4735040809 100644
--- a/Ryujinx.Graphics.Shader/CodeGen/Glsl/DefaultNames.cs
+++ b/Ryujinx.Graphics.Shader/CodeGen/Glsl/DefaultNames.cs
@@ -7,6 +7,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
         public const string SamplerNamePrefix = "tex";
         public const string ImageNamePrefix   = "img";
 
+        public const string PerPatchAttributePrefix = "patch_attr_";
         public const string IAttributePrefix = "in_attr";
         public const string OAttributePrefix = "out_attr";
 
diff --git a/Ryujinx.Graphics.Shader/CodeGen/Glsl/GlslGenerator.cs b/Ryujinx.Graphics.Shader/CodeGen/Glsl/GlslGenerator.cs
index 2d6ede0a78..077737c897 100644
--- a/Ryujinx.Graphics.Shader/CodeGen/Glsl/GlslGenerator.cs
+++ b/Ryujinx.Graphics.Shader/CodeGen/Glsl/GlslGenerator.cs
@@ -126,9 +126,10 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
 
                     string dest;
 
-                    if (assignment.Destination is AstOperand operand && operand.Type == OperandType.Attribute)
+                    if (assignment.Destination is AstOperand operand && operand.Type.IsAttribute())
                     {
-                        dest = OperandManager.GetOutAttributeName(operand.Value, context.Config);
+                        bool perPatch = operand.Type == OperandType.AttributePerPatch;
+                        dest = OperandManager.GetOutAttributeName(operand.Value, context.Config, perPatch);
                     }
                     else
                     {
diff --git a/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenMemory.cs b/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenMemory.cs
index edaacd3e4e..5e46bb46cb 100644
--- a/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenMemory.cs
+++ b/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenMemory.cs
@@ -200,7 +200,8 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
 
             if (src2 is AstOperand operand && operand.Type == OperandType.Constant)
             {
-                return OperandManager.GetAttributeName(baseAttr.Value + (operand.Value << 2), context.Config, isOutAttr: false, indexExpr);
+                int attrOffset = baseAttr.Value + (operand.Value << 2);
+                return OperandManager.GetAttributeName(attrOffset, context.Config, perPatch: false, isOutAttr: false, indexExpr);
             }
             else
             {
@@ -326,7 +327,8 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
 
             if (src2 is AstOperand operand && operand.Type == OperandType.Constant)
             {
-                attrName = OperandManager.GetAttributeName(baseAttr.Value + (operand.Value << 2), context.Config, isOutAttr: true);
+                int attrOffset = baseAttr.Value + (operand.Value << 2);
+                attrName = OperandManager.GetAttributeName(attrOffset, context.Config, perPatch: false, isOutAttr: true);
             }
             else
             {
diff --git a/Ryujinx.Graphics.Shader/CodeGen/Glsl/OperandManager.cs b/Ryujinx.Graphics.Shader/CodeGen/Glsl/OperandManager.cs
index d35525f90c..9680df270e 100644
--- a/Ryujinx.Graphics.Shader/CodeGen/Glsl/OperandManager.cs
+++ b/Ryujinx.Graphics.Shader/CodeGen/Glsl/OperandManager.cs
@@ -29,27 +29,33 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
         private static Dictionary<int, BuiltInAttribute> _builtInAttributes =
                    new Dictionary<int, BuiltInAttribute>()
         {
-            { AttributeConsts.Layer,               new BuiltInAttribute("gl_Layer",           VariableType.S32)  },
-            { AttributeConsts.PointSize,           new BuiltInAttribute("gl_PointSize",       VariableType.F32)  },
-            { AttributeConsts.PositionX,           new BuiltInAttribute("gl_Position.x",      VariableType.F32)  },
-            { AttributeConsts.PositionY,           new BuiltInAttribute("gl_Position.y",      VariableType.F32)  },
-            { AttributeConsts.PositionZ,           new BuiltInAttribute("gl_Position.z",      VariableType.F32)  },
-            { AttributeConsts.PositionW,           new BuiltInAttribute("gl_Position.w",      VariableType.F32)  },
-            { AttributeConsts.ClipDistance0,       new BuiltInAttribute("gl_ClipDistance[0]", VariableType.F32)  },
-            { AttributeConsts.ClipDistance1,       new BuiltInAttribute("gl_ClipDistance[1]", VariableType.F32)  },
-            { AttributeConsts.ClipDistance2,       new BuiltInAttribute("gl_ClipDistance[2]", VariableType.F32)  },
-            { AttributeConsts.ClipDistance3,       new BuiltInAttribute("gl_ClipDistance[3]", VariableType.F32)  },
-            { AttributeConsts.ClipDistance4,       new BuiltInAttribute("gl_ClipDistance[4]", VariableType.F32)  },
-            { AttributeConsts.ClipDistance5,       new BuiltInAttribute("gl_ClipDistance[5]", VariableType.F32)  },
-            { AttributeConsts.ClipDistance6,       new BuiltInAttribute("gl_ClipDistance[6]", VariableType.F32)  },
-            { AttributeConsts.ClipDistance7,       new BuiltInAttribute("gl_ClipDistance[7]", VariableType.F32)  },
-            { AttributeConsts.PointCoordX,         new BuiltInAttribute("gl_PointCoord.x",    VariableType.F32)  },
-            { AttributeConsts.PointCoordY,         new BuiltInAttribute("gl_PointCoord.y",    VariableType.F32)  },
-            { AttributeConsts.TessCoordX,          new BuiltInAttribute("gl_TessCoord.x",     VariableType.F32)  },
-            { AttributeConsts.TessCoordY,          new BuiltInAttribute("gl_TessCoord.y",     VariableType.F32)  },
-            { AttributeConsts.InstanceId,          new BuiltInAttribute("gl_InstanceID",      VariableType.S32)  },
-            { AttributeConsts.VertexId,            new BuiltInAttribute("gl_VertexID",        VariableType.S32)  },
-            { AttributeConsts.FrontFacing,         new BuiltInAttribute("gl_FrontFacing",     VariableType.Bool) },
+            { AttributeConsts.TessLevelOuter0, new BuiltInAttribute("gl_TessLevelOuter[0]", VariableType.F32)  },
+            { AttributeConsts.TessLevelOuter1, new BuiltInAttribute("gl_TessLevelOuter[1]", VariableType.F32)  },
+            { AttributeConsts.TessLevelOuter2, new BuiltInAttribute("gl_TessLevelOuter[2]", VariableType.F32)  },
+            { AttributeConsts.TessLevelOuter3, new BuiltInAttribute("gl_TessLevelOuter[3]", VariableType.F32)  },
+            { AttributeConsts.TessLevelInner0, new BuiltInAttribute("gl_TessLevelInner[0]", VariableType.F32)  },
+            { AttributeConsts.TessLevelInner1, new BuiltInAttribute("gl_TessLevelInner[1]", VariableType.F32)  },
+            { AttributeConsts.Layer,           new BuiltInAttribute("gl_Layer",             VariableType.S32)  },
+            { AttributeConsts.PointSize,       new BuiltInAttribute("gl_PointSize",         VariableType.F32)  },
+            { AttributeConsts.PositionX,       new BuiltInAttribute("gl_Position.x",        VariableType.F32)  },
+            { AttributeConsts.PositionY,       new BuiltInAttribute("gl_Position.y",        VariableType.F32)  },
+            { AttributeConsts.PositionZ,       new BuiltInAttribute("gl_Position.z",        VariableType.F32)  },
+            { AttributeConsts.PositionW,       new BuiltInAttribute("gl_Position.w",        VariableType.F32)  },
+            { AttributeConsts.ClipDistance0,   new BuiltInAttribute("gl_ClipDistance[0]",   VariableType.F32)  },
+            { AttributeConsts.ClipDistance1,   new BuiltInAttribute("gl_ClipDistance[1]",   VariableType.F32)  },
+            { AttributeConsts.ClipDistance2,   new BuiltInAttribute("gl_ClipDistance[2]",   VariableType.F32)  },
+            { AttributeConsts.ClipDistance3,   new BuiltInAttribute("gl_ClipDistance[3]",   VariableType.F32)  },
+            { AttributeConsts.ClipDistance4,   new BuiltInAttribute("gl_ClipDistance[4]",   VariableType.F32)  },
+            { AttributeConsts.ClipDistance5,   new BuiltInAttribute("gl_ClipDistance[5]",   VariableType.F32)  },
+            { AttributeConsts.ClipDistance6,   new BuiltInAttribute("gl_ClipDistance[6]",   VariableType.F32)  },
+            { AttributeConsts.ClipDistance7,   new BuiltInAttribute("gl_ClipDistance[7]",   VariableType.F32)  },
+            { AttributeConsts.PointCoordX,     new BuiltInAttribute("gl_PointCoord.x",      VariableType.F32)  },
+            { AttributeConsts.PointCoordY,     new BuiltInAttribute("gl_PointCoord.y",      VariableType.F32)  },
+            { AttributeConsts.TessCoordX,      new BuiltInAttribute("gl_TessCoord.x",       VariableType.F32)  },
+            { AttributeConsts.TessCoordY,      new BuiltInAttribute("gl_TessCoord.y",       VariableType.F32)  },
+            { AttributeConsts.InstanceId,      new BuiltInAttribute("gl_InstanceID",        VariableType.S32)  },
+            { AttributeConsts.VertexId,        new BuiltInAttribute("gl_VertexID",          VariableType.S32)  },
+            { AttributeConsts.FrontFacing,     new BuiltInAttribute("gl_FrontFacing",       VariableType.Bool) },
 
             // Special.
             { AttributeConsts.FragmentOutputDepth, new BuiltInAttribute("gl_FragDepth",           VariableType.F32)  },
@@ -61,6 +67,9 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
             { AttributeConsts.CtaIdY,              new BuiltInAttribute("gl_WorkGroupID.y",       VariableType.U32)  },
             { AttributeConsts.CtaIdZ,              new BuiltInAttribute("gl_WorkGroupID.z",       VariableType.U32)  },
             { AttributeConsts.LaneId,              new BuiltInAttribute(null,                     VariableType.U32)  },
+            { AttributeConsts.InvocationId,        new BuiltInAttribute("gl_InvocationID",        VariableType.S32)  },
+            { AttributeConsts.PrimitiveId,         new BuiltInAttribute("gl_PrimitiveID",         VariableType.S32)  },
+            { AttributeConsts.PatchVerticesIn,     new BuiltInAttribute("gl_PatchVerticesIn",     VariableType.S32)  },
             { AttributeConsts.EqMask,              new BuiltInAttribute(null,                     VariableType.U32)  },
             { AttributeConsts.GeMask,              new BuiltInAttribute(null,                     VariableType.U32)  },
             { AttributeConsts.GtMask,              new BuiltInAttribute(null,                     VariableType.U32)  },
@@ -99,19 +108,21 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
             return operand.Type switch
             {
                 OperandType.Argument => GetArgumentName(operand.Value),
-                OperandType.Attribute => GetAttributeName(operand.Value, config),
+                OperandType.Attribute => GetAttributeName(operand.Value, config, perPatch: false),
+                OperandType.AttributePerPatch => GetAttributeName(operand.Value, config, perPatch: true),
                 OperandType.Constant => NumberFormatter.FormatInt(operand.Value),
-                OperandType.ConstantBuffer => GetConstantBufferName(
-                    operand.CbufSlot,
-                    operand.CbufOffset,
-                    config.Stage,
-                    config.UsedFeatures.HasFlag(FeatureFlags.CbIndexing)),
+                OperandType.ConstantBuffer => GetConstantBufferName(operand, config),
                 OperandType.LocalVariable => _locals[operand],
                 OperandType.Undefined => DefaultNames.UndefinedName,
                 _ => throw new ArgumentException($"Invalid operand type \"{operand.Type}\".")
             };
         }
 
+        private static string GetConstantBufferName(AstOperand operand, ShaderConfig config)
+        {
+            return GetConstantBufferName(operand.CbufSlot, operand.CbufOffset, config.Stage, config.UsedFeatures.HasFlag(FeatureFlags.CbIndexing));
+        }
+
         public static string GetConstantBufferName(int slot, int offset, ShaderStage stage, bool cbIndexable)
         {
             return $"{GetUbName(stage, slot, cbIndexable)}[{offset >> 2}].{GetSwizzleMask(offset & 3)}";
@@ -142,14 +153,19 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
             return GetVec4Indexed(GetUbName(stage, slotExpr) + $"[{offsetExpr} >> 2]", offsetExpr + " & 3", indexElement);
         }
 
-        public static string GetOutAttributeName(int value, ShaderConfig config)
+        public static string GetOutAttributeName(int value, ShaderConfig config, bool perPatch)
         {
-            return GetAttributeName(value, config, isOutAttr: true);
+            return GetAttributeName(value, config, perPatch, isOutAttr: true);
         }
 
-        public static string GetAttributeName(int value, ShaderConfig config, bool isOutAttr = false, string indexExpr = "0")
+        public static string GetAttributeName(int value, ShaderConfig config, bool perPatch, bool isOutAttr = false, string indexExpr = "0")
         {
-            value &= ~3;
+            if ((value & AttributeConsts.LoadOutputMask) != 0)
+            {
+                isOutAttr = true;
+            }
+
+            value &= AttributeConsts.Mask & ~3;
             char swzMask = GetSwizzleMask((value >> 2) & 3);
 
             if (value >= AttributeConsts.UserAttributeBase && value < AttributeConsts.UserAttributeEnd)
@@ -160,7 +176,14 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
                     ? DefaultNames.OAttributePrefix
                     : DefaultNames.IAttributePrefix;
 
-                if (config.UsedFeatures.HasFlag(isOutAttr ? FeatureFlags.OaIndexing : FeatureFlags.IaIndexing))
+                bool indexable = config.UsedFeatures.HasFlag(isOutAttr ? FeatureFlags.OaIndexing : FeatureFlags.IaIndexing);
+
+                if (!indexable && perPatch)
+                {
+                    prefix = DefaultNames.PerPatchAttributePrefix;
+                }
+
+                if (indexable)
                 {
                     string name = prefix;
 
@@ -175,9 +198,9 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
                 {
                     string name = $"{prefix}{(value >> 4)}_{swzMask}";
 
-                    if (config.Stage == ShaderStage.Geometry && !isOutAttr)
+                    if (!perPatch && IsArrayAttribute(config.Stage, isOutAttr))
                     {
-                        name += $"[{indexExpr}]";
+                        name += isOutAttr ? "[gl_InvocationID]" : $"[{indexExpr}]";
                     }
 
                     return name;
@@ -186,9 +209,9 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
                 {
                     string name = $"{prefix}{(value >> 4)}";
 
-                    if (config.Stage == ShaderStage.Geometry && !isOutAttr)
+                    if (!perPatch && IsArrayAttribute(config.Stage, isOutAttr))
                     {
-                        name += $"[{indexExpr}]";
+                        name += isOutAttr ? "[gl_InvocationID]" : $"[{indexExpr}]";
                     }
 
                     return name + '.' + swzMask;
@@ -250,9 +273,9 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
 
                     string name = builtInAttr.Name;
 
-                    if (config.Stage == ShaderStage.Geometry && (value & AttributeConsts.SpecialMask) == 0 && !isOutAttr)
+                    if (!perPatch && IsArrayAttribute(config.Stage, isOutAttr) && IsArrayBuiltIn(value))
                     {
-                        name = $"gl_in[{indexExpr}].{name}";
+                        name = isOutAttr ? $"gl_out[gl_InvocationID].{name}" : $"gl_in[{indexExpr}].{name}";
                     }
 
                     return name;
@@ -278,6 +301,32 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
             return $"{name}[{attrExpr} >> 2][{attrExpr} & 3]";
         }
 
+        public static bool IsArrayAttribute(ShaderStage stage, bool isOutAttr)
+        {
+            if (isOutAttr)
+            {
+                return stage == ShaderStage.TessellationControl;
+            }
+            else
+            {
+                return stage == ShaderStage.TessellationControl ||
+                       stage == ShaderStage.TessellationEvaluation ||
+                       stage == ShaderStage.Geometry;
+            }
+        }
+
+        private static bool IsArrayBuiltIn(int attr)
+        {
+            if (attr <= AttributeConsts.TessLevelInner1 ||
+                attr == AttributeConsts.TessCoordX ||
+                attr == AttributeConsts.TessCoordY)
+            {
+                return false;
+            }
+
+            return (attr & AttributeConsts.SpecialMask) == 0;
+        }
+
         public static string GetUbName(ShaderStage stage, int slot, bool cbIndexable)
         {
             if (cbIndexable)
diff --git a/Ryujinx.Graphics.Shader/Decoders/Decoder.cs b/Ryujinx.Graphics.Shader/Decoders/Decoder.cs
index 656e9c44c8..b446e65047 100644
--- a/Ryujinx.Graphics.Shader/Decoders/Decoder.cs
+++ b/Ryujinx.Graphics.Shader/Decoders/Decoder.cs
@@ -262,6 +262,7 @@ namespace Ryujinx.Graphics.Shader.Decoders
             int count = 1;
             bool isStore = false;
             bool indexed = false;
+            bool perPatch = false;
 
             if (name == InstName.Ast)
             {
@@ -269,14 +270,17 @@ namespace Ryujinx.Graphics.Shader.Decoders
                 count = (int)opAst.AlSize + 1;
                 offset = opAst.Imm11;
                 indexed = opAst.Phys;
+                perPatch = opAst.P;
                 isStore = true;
             }
             else if (name == InstName.Ald)
             {
                 InstAld opAld = new InstAld(opCode);
                 count = (int)opAld.AlSize + 1;
-                indexed = opAld.Phys;
                 offset = opAld.Imm11;
+                indexed = opAld.Phys;
+                perPatch = opAld.P;
+                isStore = opAld.O;
             }
             else /* if (name == InstName.Ipa) */
             {
@@ -307,11 +311,11 @@ namespace Ryujinx.Graphics.Shader.Decoders
 
                         if (isStore)
                         {
-                            config.SetOutputUserAttribute(index);
+                            config.SetOutputUserAttribute(index, perPatch);
                         }
                         else
                         {
-                            config.SetInputUserAttribute(index);
+                            config.SetInputUserAttribute(index, perPatch);
                         }
                     }
                 }
diff --git a/Ryujinx.Graphics.Shader/Decoders/InstDecoders.cs b/Ryujinx.Graphics.Shader/Decoders/InstDecoders.cs
index ca4ff12ad7..b61412c6e3 100644
--- a/Ryujinx.Graphics.Shader/Decoders/InstDecoders.cs
+++ b/Ryujinx.Graphics.Shader/Decoders/InstDecoders.cs
@@ -5175,8 +5175,8 @@ namespace Ryujinx.Graphics.Shader.Decoders
         public int SrcB => (int)((_opcode >> 20) & 0xFF);
         public int SrcC => (int)((_opcode >> 39) & 0xFF);
         public int Pred => (int)((_opcode >> 16) & 0x7);
-        public int Imm16 => (int)((_opcode >> 20) & 0xFFFF);
         public bool PredInv => (_opcode & 0x80000) != 0;
+        public int Imm16 => (int)((_opcode >> 20) & 0xFFFF);
         public bool WriteCC => (_opcode & 0x800000000000) != 0;
         public bool DFormat => (_opcode & 0x40000000000000) != 0;
         public VectorSelect ASelect => (VectorSelect)((int)((_opcode >> 45) & 0x8) | (int)((_opcode >> 36) & 0x7));
@@ -5236,6 +5236,7 @@ namespace Ryujinx.Graphics.Shader.Decoders
         public int SrcB => (int)((_opcode >> 20) & 0xFF);
         public int Pred => (int)((_opcode >> 16) & 0x7);
         public bool PredInv => (_opcode & 0x80000) != 0;
+        public int Imm16 => (int)((_opcode >> 20) & 0xFFFF);
         public VectorSelect ASelect => (VectorSelect)((int)((_opcode >> 45) & 0x8) | (int)((_opcode >> 36) & 0x7));
         public VectorSelect BSelect => (VectorSelect)((int)((_opcode >> 46) & 0x8) | (int)((_opcode >> 28) & 0x7));
         public IComp VComp => (IComp)((int)((_opcode >> 45) & 0x4) | (int)((_opcode >> 43) & 0x3));
diff --git a/Ryujinx.Graphics.Shader/IGpuAccessor.cs b/Ryujinx.Graphics.Shader/IGpuAccessor.cs
index 6af42cf273..3fdce8ea6d 100644
--- a/Ryujinx.Graphics.Shader/IGpuAccessor.cs
+++ b/Ryujinx.Graphics.Shader/IGpuAccessor.cs
@@ -96,6 +96,21 @@ namespace Ryujinx.Graphics.Shader
             return InputTopology.Points;
         }
 
+        bool QueryTessCw()
+        {
+            return false;
+        }
+
+        TessPatchType QueryTessPatchType()
+        {
+            return TessPatchType.Triangles;
+        }
+
+        TessSpacing QueryTessSpacing()
+        {
+            return TessSpacing.EqualSpacing;
+        }
+
         TextureFormat QueryTextureFormat(int handle, int cbufSlot = -1)
         {
             return TextureFormat.R8G8B8A8Unorm;
diff --git a/Ryujinx.Graphics.Shader/Instructions/InstEmit.cs b/Ryujinx.Graphics.Shader/Instructions/InstEmit.cs
index 9cc591ca88..33c1065ae6 100644
--- a/Ryujinx.Graphics.Shader/Instructions/InstEmit.cs
+++ b/Ryujinx.Graphics.Shader/Instructions/InstEmit.cs
@@ -474,13 +474,6 @@ namespace Ryujinx.Graphics.Shader.Instructions
             context.Config.GpuAccessor.Log("Shader instruction Vset is not implemented.");
         }
 
-        public static void Vsetp(EmitterContext context)
-        {
-            InstVsetp op = context.GetOp<InstVsetp>();
-
-            context.Config.GpuAccessor.Log("Shader instruction Vsetp is not implemented.");
-        }
-
         public static void Vshl(EmitterContext context)
         {
             InstVshl op = context.GetOp<InstVshl>();
diff --git a/Ryujinx.Graphics.Shader/Instructions/InstEmitAttribute.cs b/Ryujinx.Graphics.Shader/Instructions/InstEmitAttribute.cs
index e865caf2a5..f82b835cab 100644
--- a/Ryujinx.Graphics.Shader/Instructions/InstEmitAttribute.cs
+++ b/Ryujinx.Graphics.Shader/Instructions/InstEmitAttribute.cs
@@ -40,19 +40,33 @@ namespace Ryujinx.Graphics.Shader.Instructions
 
                     context.Config.SetUsedFeature(FeatureFlags.IaIndexing);
                 }
-                else if (op.SrcB == RegisterConsts.RegisterZeroIndex)
+                else if (op.SrcB == RegisterConsts.RegisterZeroIndex || op.P)
                 {
-                    Operand src = Attribute(op.Imm11 + index * 4);
+                    int offset = op.Imm11 + index * 4;
 
-                    context.FlagAttributeRead(src.Value);
+                    context.FlagAttributeRead(offset);
+
+                    if (op.O)
+                    {
+                        offset |= AttributeConsts.LoadOutputMask;
+                    }
+
+                    Operand src = op.P ? AttributePerPatch(offset) : Attribute(offset);
 
                     context.Copy(Register(rd), src);
                 }
                 else
                 {
-                    Operand src = Const(op.Imm11 + index * 4);
+                    int offset = op.Imm11 + index * 4;
 
-                    context.FlagAttributeRead(src.Value);
+                    context.FlagAttributeRead(offset);
+
+                    if (op.O)
+                    {
+                        offset |= AttributeConsts.LoadOutputMask;
+                    }
+
+                    Operand src = Const(offset);
 
                     context.Copy(Register(rd), context.LoadAttribute(src, Const(0), primVertex));
                 }
@@ -83,9 +97,13 @@ namespace Ryujinx.Graphics.Shader.Instructions
                 }
                 else
                 {
-                    Operand dest = Attribute(op.Imm11 + index * 4);
+                    // TODO: Support indirect stores using Ra.
 
-                    context.FlagAttributeWritten(dest.Value);
+                    int offset = op.Imm11 + index * 4;
+
+                    context.FlagAttributeWritten(offset);
+
+                    Operand dest = op.P ? AttributePerPatch(offset) : Attribute(offset);
 
                     context.Copy(dest, Register(rd));
                 }
diff --git a/Ryujinx.Graphics.Shader/Instructions/InstEmitMove.cs b/Ryujinx.Graphics.Shader/Instructions/InstEmitMove.cs
index 245b22536a..240fd6b156 100644
--- a/Ryujinx.Graphics.Shader/Instructions/InstEmitMove.cs
+++ b/Ryujinx.Graphics.Shader/Instructions/InstEmitMove.cs
@@ -79,6 +79,10 @@ namespace Ryujinx.Graphics.Shader.Instructions
                     src = Attribute(AttributeConsts.LaneId);
                     break;
 
+                case SReg.InvocationId:
+                    src = Attribute(AttributeConsts.InvocationId);
+                    break;
+
                 case SReg.YDirection:
                     src = ConstF(1); // TODO: Use value from Y direction GPU register.
                     break;
@@ -87,6 +91,22 @@ namespace Ryujinx.Graphics.Shader.Instructions
                     src = context.Config.Stage == ShaderStage.Fragment ? Attribute(AttributeConsts.ThreadKill) : Const(0);
                     break;
 
+                case SReg.InvocationInfo:
+                    if (context.Config.Stage != ShaderStage.Compute && context.Config.Stage != ShaderStage.Fragment)
+                    {
+                        Operand primitiveId = Attribute(AttributeConsts.PrimitiveId);
+                        Operand patchVerticesIn = Attribute(AttributeConsts.PatchVerticesIn);
+
+                        patchVerticesIn = context.ShiftLeft(patchVerticesIn, Const(16));
+
+                        src = context.BitwiseOr(primitiveId, patchVerticesIn);
+                    }
+                    else
+                    {
+                        src = Const(0);
+                    }
+                    break;
+
                 case SReg.TId:
                     Operand tidX = Attribute(AttributeConsts.ThreadIdX);
                     Operand tidY = Attribute(AttributeConsts.ThreadIdY);
diff --git a/Ryujinx.Graphics.Shader/Instructions/InstEmitVideoMinMax.cs b/Ryujinx.Graphics.Shader/Instructions/InstEmitVideoMinMax.cs
index 890b31d669..120d6f2273 100644
--- a/Ryujinx.Graphics.Shader/Instructions/InstEmitVideoMinMax.cs
+++ b/Ryujinx.Graphics.Shader/Instructions/InstEmitVideoMinMax.cs
@@ -120,6 +120,68 @@ namespace Ryujinx.Graphics.Shader.Instructions
             context.Copy(GetDest(op.Dest), res);
         }
 
+        public static void Vsetp(EmitterContext context)
+        {
+            InstVsetp op = context.GetOp<InstVsetp>();
+
+            Operand srcA = Extend(context, GetSrcReg(context, op.SrcA), op.ASelect);
+
+            Operand srcB;
+
+            if (op.BVideo)
+            {
+                srcB = Extend(context, GetSrcReg(context, op.SrcB), op.BSelect);
+            }
+            else
+            {
+                int imm = op.Imm16;
+
+                if ((op.BSelect & VectorSelect.S8B0) != 0)
+                {
+                    imm = (imm << 16) >> 16;
+                }
+
+                srcB = Const(imm);
+            }
+
+            Operand p0Res;
+
+            bool signedA = (op.ASelect & VectorSelect.S8B0) != 0;
+            bool signedB = (op.BSelect & VectorSelect.S8B0) != 0;
+
+            if (signedA != signedB)
+            {
+                bool a32 = (op.ASelect & ~VectorSelect.S8B0) == VectorSelect.U32;
+                bool b32 = (op.BSelect & ~VectorSelect.S8B0) == VectorSelect.U32;
+
+                if (!a32 && !b32)
+                {
+                    // Both values are extended small integer and can always fit in a S32, just do a signed comparison.
+                    p0Res = GetIntComparison(context, op.VComp, srcA, srcB, isSigned: true, extended: false);
+                }
+                else
+                {
+                    // TODO: Mismatching sign case.
+                    p0Res = Const(0);
+                }
+            }
+            else
+            {
+                // Sign matches, just do a regular comparison.
+                p0Res = GetIntComparison(context, op.VComp, srcA, srcB, signedA, extended: false);
+            }
+
+            Operand p1Res = context.BitwiseNot(p0Res);
+
+            Operand pred = GetPredicate(context, op.SrcPred, op.SrcPredInv);
+
+            p0Res = InstEmitAluHelper.GetPredLogicalOp(context, op.BoolOp, p0Res, pred);
+            p1Res = InstEmitAluHelper.GetPredLogicalOp(context, op.BoolOp, p1Res, pred);
+
+            context.Copy(Register(op.DestPred, RegisterType.Predicate), p0Res);
+            context.Copy(Register(op.DestPredInv, RegisterType.Predicate), p1Res);
+        }
+
         private static Operand Extend(EmitterContext context, Operand src, VectorSelect type)
         {
             return type switch
diff --git a/Ryujinx.Graphics.Shader/IntermediateRepresentation/Instruction.cs b/Ryujinx.Graphics.Shader/IntermediateRepresentation/Instruction.cs
index b0db56f08b..03badec956 100644
--- a/Ryujinx.Graphics.Shader/IntermediateRepresentation/Instruction.cs
+++ b/Ryujinx.Graphics.Shader/IntermediateRepresentation/Instruction.cs
@@ -161,5 +161,11 @@ namespace Ryujinx.Graphics.Shader.IntermediateRepresentation
 
             return false;
         }
+
+        public static bool IsTextureQuery(this Instruction inst)
+        {
+            inst &= Instruction.Mask;
+            return inst == Instruction.Lod || inst == Instruction.TextureSize;
+        }
     }
 }
\ No newline at end of file
diff --git a/Ryujinx.Graphics.Shader/IntermediateRepresentation/OperandHelper.cs b/Ryujinx.Graphics.Shader/IntermediateRepresentation/OperandHelper.cs
index 221e278fd1..7fed861e56 100644
--- a/Ryujinx.Graphics.Shader/IntermediateRepresentation/OperandHelper.cs
+++ b/Ryujinx.Graphics.Shader/IntermediateRepresentation/OperandHelper.cs
@@ -15,6 +15,11 @@ namespace Ryujinx.Graphics.Shader.IntermediateRepresentation
             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 3427b10346..7566a03f33 100644
--- a/Ryujinx.Graphics.Shader/IntermediateRepresentation/OperandType.cs
+++ b/Ryujinx.Graphics.Shader/IntermediateRepresentation/OperandType.cs
@@ -4,6 +4,7 @@ namespace Ryujinx.Graphics.Shader.IntermediateRepresentation
     {
         Argument,
         Attribute,
+        AttributePerPatch,
         Constant,
         ConstantBuffer,
         Label,
@@ -11,4 +12,12 @@ 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/StructuredIr/OperandInfo.cs b/Ryujinx.Graphics.Shader/StructuredIr/OperandInfo.cs
index 95c5731a9f..e56008f093 100644
--- a/Ryujinx.Graphics.Shader/StructuredIr/OperandInfo.cs
+++ b/Ryujinx.Graphics.Shader/StructuredIr/OperandInfo.cs
@@ -19,15 +19,15 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
 
         public static VariableType GetVarType(OperandType type)
         {
-            switch (type)
+            return type switch
             {
-                case OperandType.Attribute:      return VariableType.F32;
-                case OperandType.Constant:       return VariableType.S32;
-                case OperandType.ConstantBuffer: return VariableType.F32;
-                case OperandType.Undefined:      return VariableType.S32;
-            }
-
-            throw new ArgumentException($"Invalid operand type \"{type}\".");
+                OperandType.Attribute => VariableType.F32,
+                OperandType.AttributePerPatch => VariableType.F32,
+                OperandType.Constant => VariableType.S32,
+                OperandType.ConstantBuffer => VariableType.F32,
+                OperandType.Undefined => VariableType.S32,
+                _ => throw new ArgumentException($"Invalid operand type \"{type}\".")
+            };
         }
     }
 }
\ No newline at end of file
diff --git a/Ryujinx.Graphics.Shader/StructuredIr/StructuredProgramContext.cs b/Ryujinx.Graphics.Shader/StructuredIr/StructuredProgramContext.cs
index a9e4417543..2a39d0210c 100644
--- a/Ryujinx.Graphics.Shader/StructuredIr/StructuredProgramContext.cs
+++ b/Ryujinx.Graphics.Shader/StructuredIr/StructuredProgramContext.cs
@@ -282,6 +282,12 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
 
         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);
+            }
+
             return GetOperand(operand);
         }
 
diff --git a/Ryujinx.Graphics.Shader/TessPatchType.cs b/Ryujinx.Graphics.Shader/TessPatchType.cs
new file mode 100644
index 0000000000..2361b69f8a
--- /dev/null
+++ b/Ryujinx.Graphics.Shader/TessPatchType.cs
@@ -0,0 +1,22 @@
+namespace Ryujinx.Graphics.Shader
+{
+    public enum TessPatchType
+    {
+        Isolines = 0,
+        Triangles = 1,
+        Quads = 2
+    }
+
+    static class TessPatchTypeExtensions
+    {
+        public static string ToGlsl(this TessPatchType type)
+        {
+            return type switch
+            {
+                TessPatchType.Isolines => "isolines",
+                TessPatchType.Quads => "quads",
+                _ => "triangles"
+            };
+        }
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.Graphics.Shader/TessSpacing.cs b/Ryujinx.Graphics.Shader/TessSpacing.cs
new file mode 100644
index 0000000000..35c44190c4
--- /dev/null
+++ b/Ryujinx.Graphics.Shader/TessSpacing.cs
@@ -0,0 +1,22 @@
+namespace Ryujinx.Graphics.Shader
+{
+    public enum TessSpacing
+    {
+        EqualSpacing = 0,
+        FractionalEventSpacing = 1,
+        FractionalOddSpacing = 2
+    }
+
+    static class TessSpacingExtensions
+    {
+        public static string ToGlsl(this TessSpacing spacing)
+        {
+            return spacing switch
+            {
+                TessSpacing.FractionalEventSpacing => "fractional_even_spacing",
+                TessSpacing.FractionalOddSpacing => "fractional_odd_spacing",
+                _ => "equal_spacing"
+            };
+        }
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.Graphics.Shader/Translation/AttributeConsts.cs b/Ryujinx.Graphics.Shader/Translation/AttributeConsts.cs
index 3d0d216ee3..128013d8c8 100644
--- a/Ryujinx.Graphics.Shader/Translation/AttributeConsts.cs
+++ b/Ryujinx.Graphics.Shader/Translation/AttributeConsts.cs
@@ -2,36 +2,45 @@ namespace Ryujinx.Graphics.Shader.Translation
 {
     static class AttributeConsts
     {
-        public const int Layer         = 0x064;
-        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 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 FrontFacing   = 0x3fc;
+        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 Layer           = 0x064;
+        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 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 FrontFacing     = 0x3fc;
 
         public const int UserAttributesCount = 32;
         public const int UserAttributeBase   = 0x80;
         public const int UserAttributeEnd    = UserAttributeBase + UserAttributesCount * 16;
 
+        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             = 0xff << 24;
+        public const int SpecialMask             = 0xf << 24;
         public const int FragmentOutputDepth     = 0x1000000;
         public const int FragmentOutputColorBase = 0x1000010;
         public const int FragmentOutputColorEnd  = FragmentOutputColorBase + 8 * 16;
@@ -49,12 +58,16 @@ namespace Ryujinx.Graphics.Shader.Translation
 
         public const int LaneId = 0x2000020;
 
-        public const int EqMask = 0x2000024;
-        public const int GeMask = 0x2000028;
-        public const int GtMask = 0x200002c;
-        public const int LeMask = 0x2000030;
-        public const int LtMask = 0x2000034;
+        public const int InvocationId = 0x2000024;
+        public const int PrimitiveId = 0x2000028;
+        public const int PatchVerticesIn = 0x200002c;
 
-        public const int ThreadKill = 0x2000038;
+        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;
     }
 }
\ No newline at end of file
diff --git a/Ryujinx.Graphics.Shader/Translation/EmitterContext.cs b/Ryujinx.Graphics.Shader/Translation/EmitterContext.cs
index bdfd9626a2..6f2a6c3b3f 100644
--- a/Ryujinx.Graphics.Shader/Translation/EmitterContext.cs
+++ b/Ryujinx.Graphics.Shader/Translation/EmitterContext.cs
@@ -216,7 +216,7 @@ namespace Ryujinx.Graphics.Shader.Translation
 
                     if (target.Enabled)
                     {
-                        Config.SetOutputUserAttribute(rtIndex);
+                        Config.SetOutputUserAttribute(rtIndex, perPatch: false);
                         regIndexBase += 4;
                     }
                 }
diff --git a/Ryujinx.Graphics.Shader/Translation/ShaderConfig.cs b/Ryujinx.Graphics.Shader/Translation/ShaderConfig.cs
index 72fa7733fd..ec7e898237 100644
--- a/Ryujinx.Graphics.Shader/Translation/ShaderConfig.cs
+++ b/Ryujinx.Graphics.Shader/Translation/ShaderConfig.cs
@@ -15,6 +15,8 @@ namespace Ryujinx.Graphics.Shader.Translation
 
         public bool GpPassthrough { get; }
 
+        public int ThreadsPerInputPrimitive { get; }
+
         public OutputTopology OutputTopology { get; }
 
         public int MaxOutputVertices { get; }
@@ -42,7 +44,9 @@ namespace Ryujinx.Graphics.Shader.Translation
         private readonly TranslationCounts _counts;
 
         public int UsedInputAttributes { get; private set; }
+        public int UsedInputAttributesPerPatch { get; private set; }
         public int UsedOutputAttributes { get; private set; }
+        public int UsedOutputAttributesPerPatch { get; private set; }
         public int PassthroughAttributes { get; private set; }
 
         private int _usedConstantBuffers;
@@ -111,15 +115,16 @@ namespace Ryujinx.Graphics.Shader.Translation
 
         public ShaderConfig(ShaderHeader header, IGpuAccessor gpuAccessor, TranslationOptions options, TranslationCounts counts) : this(gpuAccessor, options, counts)
         {
-            Stage             = header.Stage;
-            GpPassthrough     = header.Stage == ShaderStage.Geometry && header.GpPassthrough;
-            OutputTopology    = header.OutputTopology;
-            MaxOutputVertices = header.MaxOutputVertexCount;
-            LocalMemorySize   = header.ShaderLocalMemoryLowSize + header.ShaderLocalMemoryHighSize;
-            ImapTypes         = header.ImapTypes;
-            OmapTargets       = header.OmapTargets;
-            OmapSampleMask    = header.OmapSampleMask;
-            OmapDepth         = header.OmapDepth;
+            Stage                    = header.Stage;
+            GpPassthrough            = header.Stage == ShaderStage.Geometry && header.GpPassthrough;
+            ThreadsPerInputPrimitive = header.ThreadsPerInputPrimitive;
+            OutputTopology           = header.OutputTopology;
+            MaxOutputVertices        = header.MaxOutputVertexCount;
+            LocalMemorySize          = header.ShaderLocalMemoryLowSize + header.ShaderLocalMemoryHighSize;
+            ImapTypes                = header.ImapTypes;
+            OmapTargets              = header.OmapTargets;
+            OmapSampleMask           = header.OmapSampleMask;
+            OmapDepth                = header.OmapDepth;
         }
 
         public int GetDepthRegister()
@@ -169,7 +174,7 @@ namespace Ryujinx.Graphics.Shader.Translation
 
         public TextureFormat GetTextureFormatAtomic(int handle, int cbufSlot = -1)
         {
-            // Atomic image instructions do not support GL_EXT_shader_image_load_formatted, 
+            // Atomic image instructions do not support GL_EXT_shader_image_load_formatted,
             // and must have a type specified. Default to R32Sint if not available.
 
             var format = GpuAccessor.QueryTextureFormat(handle, cbufSlot);
@@ -219,17 +224,31 @@ namespace Ryujinx.Graphics.Shader.Translation
             }
         }
 
-        public void SetInputUserAttribute(int index)
+        public void SetInputUserAttribute(int index, bool perPatch)
         {
-            UsedInputAttributes |= 1 << index;
+            if (perPatch)
+            {
+                UsedInputAttributesPerPatch |= 1 << index;
+            }
+            else
+            {
+                UsedInputAttributes |= 1 << index;
+            }
         }
 
-        public void SetOutputUserAttribute(int index)
+        public void SetOutputUserAttribute(int index, bool perPatch)
         {
-            UsedOutputAttributes |= 1 << index;
+            if (perPatch)
+            {
+                UsedOutputAttributesPerPatch |= 1 << index;
+            }
+            else
+            {
+                UsedOutputAttributes |= 1 << index;
+            }
         }
 
-        public void MergeOutputUserAttributes(int mask)
+        public void MergeOutputUserAttributes(int mask, int maskPerPatch)
         {
             if (GpPassthrough)
             {
@@ -238,6 +257,7 @@ namespace Ryujinx.Graphics.Shader.Translation
             else
             {
                 UsedOutputAttributes |= mask;
+                UsedOutputAttributesPerPatch |= maskPerPatch;
             }
         }
 
diff --git a/Ryujinx.Graphics.Shader/Translation/Translator.cs b/Ryujinx.Graphics.Shader/Translation/Translator.cs
index 0a0ee4a715..0243eba139 100644
--- a/Ryujinx.Graphics.Shader/Translation/Translator.cs
+++ b/Ryujinx.Graphics.Shader/Translation/Translator.cs
@@ -216,27 +216,38 @@ namespace Ryujinx.Graphics.Shader.Translation
                 return;
             }
 
-            void InitializeOutput(int baseAttr)
-            {
-                for (int c = 0; c < 4; c++)
-                {
-                    context.Copy(Attribute(baseAttr + c * 4), ConstF(c == 3 ? 1f : 0f));
-                }
-            }
-
             if (config.Stage == ShaderStage.Vertex)
             {
-                InitializeOutput(AttributeConsts.PositionX);
+                InitializeOutput(context, AttributeConsts.PositionX, perPatch: false);
             }
 
-            int usedAttribtes = context.Config.UsedOutputAttributes;
-            while (usedAttribtes != 0)
+            int usedAttributes = context.Config.UsedOutputAttributes;
+            while (usedAttributes != 0)
             {
-                int index = BitOperations.TrailingZeroCount(usedAttribtes);
+                int index = BitOperations.TrailingZeroCount(usedAttributes);
 
-                InitializeOutput(AttributeConsts.UserAttributeBase + index * 16);
+                InitializeOutput(context, AttributeConsts.UserAttributeBase + index * 16, perPatch: false);
 
-                usedAttribtes &= ~(1 << index);
+                usedAttributes &= ~(1 << index);
+            }
+
+            int usedAttributesPerPatch = context.Config.UsedOutputAttributesPerPatch;
+            while (usedAttributesPerPatch != 0)
+            {
+                int index = BitOperations.TrailingZeroCount(usedAttributesPerPatch);
+
+                InitializeOutput(context, AttributeConsts.UserAttributeBase + index * 16, perPatch: true);
+
+                usedAttributesPerPatch &= ~(1 << index);
+            }
+        }
+
+        private static void InitializeOutput(EmitterContext context, int baseAttr, bool perPatch)
+        {
+            for (int c = 0; c < 4; c++)
+            {
+                int attrOffset = baseAttr + c * 4;
+                context.Copy(perPatch ? AttributePerPatch(attrOffset) : Attribute(attrOffset), ConstF(c == 3 ? 1f : 0f));
             }
         }
 
diff --git a/Ryujinx.Graphics.Shader/Translation/TranslatorContext.cs b/Ryujinx.Graphics.Shader/Translation/TranslatorContext.cs
index 3c7b3c2bea..34b116d6b5 100644
--- a/Ryujinx.Graphics.Shader/Translation/TranslatorContext.cs
+++ b/Ryujinx.Graphics.Shader/Translation/TranslatorContext.cs
@@ -32,10 +32,13 @@ namespace Ryujinx.Graphics.Shader.Translation
 
         private static bool IsUserAttribute(Operand operand)
         {
-            return operand != null &&
-                   operand.Type == OperandType.Attribute &&
-                   operand.Value >= AttributeConsts.UserAttributeBase &&
-                   operand.Value < AttributeConsts.UserAttributeEnd;
+            if (operand != null && operand.Type.IsAttribute())
+            {
+                int value = operand.Value & AttributeConsts.Mask;
+                return value >= AttributeConsts.UserAttributeBase && value < AttributeConsts.UserAttributeEnd;
+            }
+
+            return false;
         }
 
         private static FunctionCode[] Combine(FunctionCode[] a, FunctionCode[] b, int aStart)
@@ -133,14 +136,16 @@ namespace Ryujinx.Graphics.Shader.Translation
         {
             if (nextStage != null)
             {
-                _config.MergeOutputUserAttributes(nextStage._config.UsedInputAttributes);
+                _config.MergeOutputUserAttributes(
+                    nextStage._config.UsedInputAttributes,
+                    nextStage._config.UsedInputAttributesPerPatch);
             }
 
             FunctionCode[] code = EmitShader(_cfg, _config, initializeOutputs: other == null, out _);
 
             if (other != null)
             {
-                other._config.MergeOutputUserAttributes(_config.UsedOutputAttributes);
+                other._config.MergeOutputUserAttributes(_config.UsedOutputAttributes, 0);
 
                 FunctionCode[] otherCode = EmitShader(other._cfg, other._config, initializeOutputs: true, out int aStart);