diff --git a/Ryujinx.Graphics.GAL/IPipeline.cs b/Ryujinx.Graphics.GAL/IPipeline.cs
index 3eb778e08e..7c67a1720f 100644
--- a/Ryujinx.Graphics.GAL/IPipeline.cs
+++ b/Ryujinx.Graphics.GAL/IPipeline.cs
@@ -7,6 +7,8 @@ namespace Ryujinx.Graphics.GAL
     {
         void Barrier();
 
+        void BeginTransformFeedback(PrimitiveTopology topology);
+
         void ClearRenderTargetColor(int index, uint componentMask, ColorF color);
 
         void ClearRenderTargetDepthStencil(
@@ -27,6 +29,8 @@ namespace Ryujinx.Graphics.GAL
             int firstVertex,
             int firstInstance);
 
+        void EndTransformFeedback();
+
         void SetBlendState(int index, BlendDescriptor blend);
 
         void SetDepthBias(PolygonModeMask enables, float factor, float units, float clamp);
@@ -73,6 +77,7 @@ namespace Ryujinx.Graphics.GAL
 
         void SetTexture(int index, ShaderStage stage, ITexture texture);
 
+        void SetTransformFeedbackBuffer(int index, BufferRange buffer);
         void SetUniformBuffer(int index, ShaderStage stage, BufferRange buffer);
 
         void SetUserClipDistance(int index, bool enableClip);
diff --git a/Ryujinx.Graphics.GAL/IRenderer.cs b/Ryujinx.Graphics.GAL/IRenderer.cs
index 1052f1476c..6fd3feba60 100644
--- a/Ryujinx.Graphics.GAL/IRenderer.cs
+++ b/Ryujinx.Graphics.GAL/IRenderer.cs
@@ -13,7 +13,7 @@ namespace Ryujinx.Graphics.GAL
 
         BufferHandle CreateBuffer(int size);
 
-        IProgram CreateProgram(IShader[] shaders);
+        IProgram CreateProgram(IShader[] shaders, TransformFeedbackDescriptor[] transformFeedbackDescriptors);
 
         ISampler CreateSampler(SamplerCreateInfo info);
         ITexture CreateTexture(TextureCreateInfo info, float scale);
diff --git a/Ryujinx.Graphics.GAL/TransformFeedbackDescriptor.cs b/Ryujinx.Graphics.GAL/TransformFeedbackDescriptor.cs
new file mode 100644
index 0000000000..690b9108ed
--- /dev/null
+++ b/Ryujinx.Graphics.GAL/TransformFeedbackDescriptor.cs
@@ -0,0 +1,19 @@
+using System;
+
+namespace Ryujinx.Graphics.GAL
+{
+    public struct TransformFeedbackDescriptor
+    {
+        public int BufferIndex { get; }
+        public int Stride      { get; }
+
+        public byte[] VaryingLocations { get; }
+
+        public TransformFeedbackDescriptor(int bufferIndex, int stride, byte[] varyingLocations)
+        {
+            BufferIndex      = bufferIndex;
+            Stride           = stride;
+            VaryingLocations = varyingLocations ?? throw new ArgumentNullException(nameof(varyingLocations));
+        }
+    }
+}
diff --git a/Ryujinx.Graphics.Gpu/Constants.cs b/Ryujinx.Graphics.Gpu/Constants.cs
index ac6b6139c0..231152f5ea 100644
--- a/Ryujinx.Graphics.Gpu/Constants.cs
+++ b/Ryujinx.Graphics.Gpu/Constants.cs
@@ -35,6 +35,11 @@ namespace Ryujinx.Graphics.Gpu
         /// </remarks>
         public const int TotalGpStorageBuffers = 16;
 
+        /// <summary>
+        /// Maximum number of transform feedback buffers.
+        /// </summary>
+        public const int TotalTransformFeedbackBuffers = 4;
+
         /// <summary>
         /// Maximum number of render target color buffers.
         /// </summary>
diff --git a/Ryujinx.Graphics.Gpu/Engine/MethodReport.cs b/Ryujinx.Graphics.Gpu/Engine/MethodReport.cs
index 224a4da181..fcea438918 100644
--- a/Ryujinx.Graphics.Gpu/Engine/MethodReport.cs
+++ b/Ryujinx.Graphics.Gpu/Engine/MethodReport.cs
@@ -61,8 +61,6 @@ namespace Ryujinx.Graphics.Gpu.Engine
         /// <param name="type">Counter to be written to memory</param>
         private void ReportCounter(GpuState state, ReportCounterType type)
         {
-            CounterData counterData = new CounterData();
-
             var rs = state.Get<SemaphoreState>(MethodOffset.ReportState);
 
             ulong gpuVa = rs.Address.Pack();
@@ -80,16 +78,14 @@ namespace Ryujinx.Graphics.Gpu.Engine
 
             EventHandler<ulong> resultHandler = (object evt, ulong result) =>
             {
+                CounterData counterData = new CounterData();
+
                 counterData.Counter = result;
                 counterData.Timestamp = ticks;
 
-                Span<CounterData> counterDataSpan = MemoryMarshal.CreateSpan(ref counterData, 1);
-
-                Span<byte> data = MemoryMarshal.Cast<CounterData, byte>(counterDataSpan);
-
                 if (counter?.Invalid != true)
                 {
-                    _context.MemoryAccessor.Write(gpuVa, data);
+                    _context.MemoryAccessor.Write(gpuVa, counterData);
                 }
             };
 
diff --git a/Ryujinx.Graphics.Gpu/Engine/Methods.cs b/Ryujinx.Graphics.Gpu/Engine/Methods.cs
index d5b11c2cd8..093f90488d 100644
--- a/Ryujinx.Graphics.Gpu/Engine/Methods.cs
+++ b/Ryujinx.Graphics.Gpu/Engine/Methods.cs
@@ -40,6 +40,8 @@ namespace Ryujinx.Graphics.Gpu.Engine
 
         private bool _forceShaderUpdate;
 
+        private bool _prevTfEnable;
+
         /// <summary>
         /// Creates a new instance of the GPU methods class.
         /// </summary>
@@ -124,6 +126,14 @@ namespace Ryujinx.Graphics.Gpu.Engine
         /// <param name="state">Guest GPU state</param>
         private void UpdateState(GpuState state)
         {
+            bool tfEnable = state.Get<Boolean32>(MethodOffset.TfEnable);
+
+            if (!tfEnable && _prevTfEnable)
+            {
+                _context.Renderer.Pipeline.EndTransformFeedback();
+                _prevTfEnable = false;
+            }
+
             // Shaders must be the first one to be updated if modified, because
             // some of the other state depends on information from the currently
             // bound shaders.
@@ -134,6 +144,11 @@ namespace Ryujinx.Graphics.Gpu.Engine
                 UpdateShaderState(state);
             }
 
+            if (state.QueryModified(MethodOffset.TfBufferState))
+            {
+                UpdateTfBufferState(state);
+            }
+
             if (state.QueryModified(MethodOffset.ClipDistanceEnable))
             {
                 UpdateUserClipState(state);
@@ -258,6 +273,12 @@ namespace Ryujinx.Graphics.Gpu.Engine
             }
 
             CommitBindings();
+
+            if (tfEnable && !_prevTfEnable)
+            {
+                _context.Renderer.Pipeline.BeginTransformFeedback(PrimitiveType.Convert());
+                _prevTfEnable = true;
+            }
         }
 
         /// <summary>
@@ -318,7 +339,7 @@ namespace Ryujinx.Graphics.Gpu.Engine
         /// </summary>
         /// <param name="state">Current GPU state</param>
         /// <param name="useControl">Use draw buffers information from render target control register</param>
-        /// <param name="singleUse">If this is not -1, it indicates that only the given indexed target will be used.</param> 
+        /// <param name="singleUse">If this is not -1, it indicates that only the given indexed target will be used.</param>
         private void UpdateRenderTargetState(GpuState state, bool useControl, int singleUse = -1)
         {
             var rtControl = state.Get<RtControl>(MethodOffset.RtControl);
@@ -1003,6 +1024,27 @@ namespace Ryujinx.Graphics.Gpu.Engine
             _context.Renderer.Pipeline.SetProgram(gs.HostProgram);
         }
 
+        /// <summary>
+        /// Updates transform feedback buffer state based on the guest GPU state.
+        /// </summary>
+        /// <param name="state">Current GPU state</param>
+        private void UpdateTfBufferState(GpuState state)
+        {
+            for (int index = 0; index < Constants.TotalTransformFeedbackBuffers; index++)
+            {
+                TfBufferState tfb = state.Get<TfBufferState>(MethodOffset.TfBufferState, index);
+
+                if (!tfb.Enable)
+                {
+                    BufferManager.SetTransformFeedbackBuffer(index, 0, 0);
+
+                    continue;
+                }
+
+                BufferManager.SetTransformFeedbackBuffer(index, tfb.Address.Pack(), (uint)tfb.Size);
+            }
+        }
+
         /// <summary>
         /// Updates user-defined clipping based on the guest GPU state.
         /// </summary>
diff --git a/Ryujinx.Graphics.Gpu/Memory/BufferManager.cs b/Ryujinx.Graphics.Gpu/Memory/BufferManager.cs
index 533b05768f..9712f58fb4 100644
--- a/Ryujinx.Graphics.Gpu/Memory/BufferManager.cs
+++ b/Ryujinx.Graphics.Gpu/Memory/BufferManager.cs
@@ -24,8 +24,8 @@ namespace Ryujinx.Graphics.Gpu.Memory
         private Buffer[] _bufferOverlaps;
 
         private IndexBuffer _indexBuffer;
-
         private VertexBuffer[] _vertexBuffers;
+        private BufferBounds[] _transformFeedbackBuffers;
 
         private class BuffersPerStage
         {
@@ -56,6 +56,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
         private bool _indexBufferDirty;
         private bool _vertexBuffersDirty;
         private uint _vertexBuffersEnableMask;
+        private bool _transformFeedbackBuffersDirty;
 
         private bool _rebind;
 
@@ -73,6 +74,8 @@ namespace Ryujinx.Graphics.Gpu.Memory
 
             _vertexBuffers = new VertexBuffer[Constants.TotalVertexBuffers];
 
+            _transformFeedbackBuffers = new BufferBounds[Constants.TotalTransformFeedbackBuffers];
+
             _cpStorageBuffers = new BuffersPerStage(Constants.TotalCpStorageBuffers);
             _cpUniformBuffers = new BuffersPerStage(Constants.TotalCpUniformBuffers);
 
@@ -144,6 +147,16 @@ namespace Ryujinx.Graphics.Gpu.Memory
             }
         }
 
+        public void SetTransformFeedbackBuffer(int index, ulong gpuVa, ulong size)
+        {
+            ulong address = TranslateAndCreateBuffer(gpuVa, size);
+
+            _transformFeedbackBuffers[index].Address = address;
+            _transformFeedbackBuffers[index].Size    = size;
+
+            _transformFeedbackBuffersDirty = true;
+        }
+
         /// <summary>
         /// Sets a storage buffer on the compute pipeline.
         /// Storage buffers can be read and written to on shaders.
@@ -522,6 +535,41 @@ namespace Ryujinx.Graphics.Gpu.Memory
                 }
             }
 
+            if (_transformFeedbackBuffersDirty)
+            {
+                _transformFeedbackBuffersDirty = false;
+
+                for (int index = 0; index < Constants.TotalTransformFeedbackBuffers; index++)
+                {
+                    BufferBounds tfb = _transformFeedbackBuffers[index];
+
+                    if (tfb.Address == 0)
+                    {
+                        _context.Renderer.Pipeline.SetTransformFeedbackBuffer(index, new BufferRange(BufferHandle.Null, 0, 0));
+
+                        continue;
+                    }
+
+                    BufferRange buffer = GetBufferRange(tfb.Address, tfb.Size);
+
+                    _context.Renderer.Pipeline.SetTransformFeedbackBuffer(index, buffer);
+                }
+            }
+            else
+            {
+                for (int index = 0; index < Constants.TotalTransformFeedbackBuffers; index++)
+                {
+                    BufferBounds tfb = _transformFeedbackBuffers[index];
+
+                    if (tfb.Address == 0)
+                    {
+                        continue;
+                    }
+
+                    SynchronizeBufferRange(tfb.Address, tfb.Size);
+                }
+            }
+
             if (_gpStorageBuffersDirty || _rebind)
             {
                 _gpStorageBuffersDirty = false;
diff --git a/Ryujinx.Graphics.Gpu/Memory/MemoryAccessor.cs b/Ryujinx.Graphics.Gpu/Memory/MemoryAccessor.cs
index 5cc8ec24f9..2c53f6991f 100644
--- a/Ryujinx.Graphics.Gpu/Memory/MemoryAccessor.cs
+++ b/Ryujinx.Graphics.Gpu/Memory/MemoryAccessor.cs
@@ -63,11 +63,11 @@ namespace Ryujinx.Graphics.Gpu.Memory
         /// </summary>
         /// <param name="gpuVa">GPU virtual address to write the value into</param>
         /// <param name="value">The value to be written</param>
-        public void Write(ulong gpuVa, int value)
+        public void Write<T>(ulong gpuVa, T value) where T : unmanaged
         {
             ulong processVa = _context.MemoryManager.Translate(gpuVa);
 
-            _context.PhysicalMemory.Write(processVa, BitConverter.GetBytes(value));
+            _context.PhysicalMemory.Write(processVa, MemoryMarshal.Cast<T, byte>(MemoryMarshal.CreateSpan(ref value, 1)));
         }
 
         /// <summary>
diff --git a/Ryujinx.Graphics.Gpu/Shader/ShaderCache.cs b/Ryujinx.Graphics.Gpu/Shader/ShaderCache.cs
index 8a1abe32f3..d16f105787 100644
--- a/Ryujinx.Graphics.Gpu/Shader/ShaderCache.cs
+++ b/Ryujinx.Graphics.Gpu/Shader/ShaderCache.cs
@@ -82,7 +82,7 @@ namespace Ryujinx.Graphics.Gpu.Shader
 
             shader.HostShader = _context.Renderer.CompileShader(shader.Program);
 
-            IProgram hostProgram = _context.Renderer.CreateProgram(new IShader[] { shader.HostShader });
+            IProgram hostProgram = _context.Renderer.CreateProgram(new IShader[] { shader.HostShader }, null);
 
             ShaderBundle cpShader = new ShaderBundle(hostProgram, shader);
 
@@ -150,6 +150,8 @@ namespace Ryujinx.Graphics.Gpu.Shader
                     continue;
                 }
 
+                var tfd = GetTransformFeedbackDescriptors(state);
+
                 IShader hostShader = _context.Renderer.CompileShader(program);
 
                 shaders[stage].HostShader = hostShader;
@@ -157,7 +159,7 @@ namespace Ryujinx.Graphics.Gpu.Shader
                 hostShaders.Add(hostShader);
             }
 
-            IProgram hostProgram = _context.Renderer.CreateProgram(hostShaders.ToArray());
+            IProgram hostProgram = _context.Renderer.CreateProgram(hostShaders.ToArray(), GetTransformFeedbackDescriptors(state));
 
             ShaderBundle gpShaders = new ShaderBundle(hostProgram, shaders);
 
@@ -173,6 +175,36 @@ namespace Ryujinx.Graphics.Gpu.Shader
             return gpShaders;
         }
 
+        /// <summary>
+        /// Gets transform feedback state from the current GPU state.
+        /// </summary>
+        /// <param name="state">Current GPU state</param>
+        /// <returns>Four transform feedback descriptors for the enabled TFBs, or null if TFB is disabled</returns>
+        private TransformFeedbackDescriptor[] GetTransformFeedbackDescriptors(GpuState state)
+        {
+            bool tfEnable = state.Get<Boolean32>(MethodOffset.TfEnable);
+
+            if (!tfEnable)
+            {
+                return null;
+            }
+
+            TransformFeedbackDescriptor[] descs = new TransformFeedbackDescriptor[Constants.TotalTransformFeedbackBuffers];
+
+            for (int i = 0; i < Constants.TotalTransformFeedbackBuffers; i++)
+            {
+                var tf = state.Get<TfState>(MethodOffset.TfState, i);
+
+                int length = (int)Math.Min((uint)tf.VaryingsCount, 0x80);
+
+                var varyingLocations = state.GetSpan(MethodOffset.TfVaryingLocations + i * 0x80, length).ToArray();
+
+                descs[i] = new TransformFeedbackDescriptor(tf.BufferIndex, tf.Stride, varyingLocations);
+            }
+
+            return descs;
+        }
+
         /// <summary>
         /// Checks if compute shader code in memory is equal to the cached shader.
         /// </summary>
diff --git a/Ryujinx.Graphics.Gpu/State/GpuState.cs b/Ryujinx.Graphics.Gpu/State/GpuState.cs
index 264719b443..9e7d9492a6 100644
--- a/Ryujinx.Graphics.Gpu/State/GpuState.cs
+++ b/Ryujinx.Graphics.Gpu/State/GpuState.cs
@@ -346,7 +346,7 @@ namespace Ryujinx.Graphics.Gpu.State
         /// <param name="offset">Register offset</param>
         /// <param name="index">Index for indexed data</param>
         /// <returns>The data at the specified location</returns>
-        public T Get<T>(MethodOffset offset, int index) where T : struct
+        public T Get<T>(MethodOffset offset, int index) where T : unmanaged
         {
             Register register = _registers[(int)offset];
 
@@ -364,11 +364,22 @@ namespace Ryujinx.Graphics.Gpu.State
         /// <typeparam name="T">Type of the data</typeparam>
         /// <param name="offset">Register offset</param>
         /// <returns>The data at the specified location</returns>
-        public T Get<T>(MethodOffset offset) where T : struct
+        public T Get<T>(MethodOffset offset) where T : unmanaged
         {
             return MemoryMarshal.Cast<int, T>(_memory.AsSpan().Slice((int)offset))[0];
         }
 
+        /// <summary>
+        /// Gets a span of the data at a given register offset.
+        /// </summary>
+        /// <param name="offset">Register offset</param>
+        /// <param name="length">Length of the data in bytes</param>
+        /// <returns>The data at the specified location</returns>
+        public Span<byte> GetSpan(MethodOffset offset, int length)
+        {
+            return MemoryMarshal.Cast<int, byte>(_memory.AsSpan().Slice((int)offset)).Slice(0, length);
+        }
+
         /// <summary>
         /// Sets indexed data to a given register offset.
         /// </summary>
@@ -376,7 +387,7 @@ namespace Ryujinx.Graphics.Gpu.State
         /// <param name="offset">Register offset</param>
         /// <param name="index">Index for indexed data</param>
         /// <param name="data">The data to set</param>
-        public void Set<T>(MethodOffset offset, int index, T data) where T : struct
+        public void Set<T>(MethodOffset offset, int index, T data) where T : unmanaged
         {
             Register register = _registers[(int)offset];
 
@@ -394,7 +405,7 @@ namespace Ryujinx.Graphics.Gpu.State
         /// <typeparam name="T">Type of the data</typeparam>
         /// <param name="offset">Register offset</param>
         /// <param name="data">The data to set</param>
-        public void Set<T>(MethodOffset offset, T data) where T : struct
+        public void Set<T>(MethodOffset offset, T data) where T : unmanaged
         {
             ReadOnlySpan<int> intSpan = MemoryMarshal.Cast<T, int>(MemoryMarshal.CreateReadOnlySpan(ref data, 1));
             intSpan.CopyTo(_memory.AsSpan().Slice((int)offset, intSpan.Length));
diff --git a/Ryujinx.Graphics.Gpu/State/GpuStateTable.cs b/Ryujinx.Graphics.Gpu/State/GpuStateTable.cs
index 65df5f4e67..acc0fe858d 100644
--- a/Ryujinx.Graphics.Gpu/State/GpuStateTable.cs
+++ b/Ryujinx.Graphics.Gpu/State/GpuStateTable.cs
@@ -53,32 +53,34 @@ namespace Ryujinx.Graphics.Gpu.State
         /// </summary>
         public static TableItem[] Table = new TableItem[]
         {
-            new TableItem(MethodOffset.RtColorState,           typeof(RtColorState),          Constants.TotalRenderTargets),
-            new TableItem(MethodOffset.ViewportTransform,      typeof(ViewportTransform),     Constants.TotalViewports),
-            new TableItem(MethodOffset.ViewportExtents,        typeof(ViewportExtents),       Constants.TotalViewports),
-            new TableItem(MethodOffset.VertexBufferDrawState,  typeof(VertexBufferDrawState), 1),
-            new TableItem(MethodOffset.DepthBiasState,         typeof(DepthBiasState),        1),
-            new TableItem(MethodOffset.ScissorState,           typeof(ScissorState),          Constants.TotalViewports),
-            new TableItem(MethodOffset.StencilBackMasks,       typeof(StencilBackMasks),      1),
-            new TableItem(MethodOffset.RtDepthStencilState,    typeof(RtDepthStencilState),   1),
-            new TableItem(MethodOffset.VertexAttribState,      typeof(VertexAttribState),     16),
-            new TableItem(MethodOffset.RtDepthStencilSize,     typeof(Size3D),                1),
-            new TableItem(MethodOffset.BlendEnable,            typeof(Boolean32),             Constants.TotalRenderTargets),
-            new TableItem(MethodOffset.StencilTestState,       typeof(StencilTestState),      1),
-            new TableItem(MethodOffset.SamplerPoolState,       typeof(PoolState),             1),
-            new TableItem(MethodOffset.TexturePoolState,       typeof(PoolState),             1),
-            new TableItem(MethodOffset.StencilBackTestState,   typeof(StencilBackTestState),  1),
-            new TableItem(MethodOffset.ShaderBaseAddress,      typeof(GpuVa),                 1),
-            new TableItem(MethodOffset.PrimitiveRestartState,  typeof(PrimitiveRestartState), 1),
-            new TableItem(MethodOffset.IndexBufferState,       typeof(IndexBufferState),      1),
-            new TableItem(MethodOffset.VertexBufferInstanced,  typeof(Boolean32),             16),
-            new TableItem(MethodOffset.FaceState,              typeof(FaceState),             1),
-            new TableItem(MethodOffset.RtColorMask,            typeof(RtColorMask),           Constants.TotalRenderTargets),
-            new TableItem(MethodOffset.VertexBufferState,      typeof(VertexBufferState),     16),
-            new TableItem(MethodOffset.BlendConstant,          typeof(ColorF),                1),
-            new TableItem(MethodOffset.BlendState,             typeof(BlendState),            Constants.TotalRenderTargets),
-            new TableItem(MethodOffset.VertexBufferEndAddress, typeof(GpuVa),                 16),
-            new TableItem(MethodOffset.ShaderState,            typeof(ShaderState),           6),
+            new TableItem(MethodOffset.TfBufferState,          typeof(TfBufferState),          Constants.TotalTransformFeedbackBuffers),
+            new TableItem(MethodOffset.TfState,                typeof(TfState),                Constants.TotalTransformFeedbackBuffers),
+            new TableItem(MethodOffset.RtColorState,           typeof(RtColorState),           Constants.TotalRenderTargets),
+            new TableItem(MethodOffset.ViewportTransform,      typeof(ViewportTransform),      Constants.TotalViewports),
+            new TableItem(MethodOffset.ViewportExtents,        typeof(ViewportExtents),        Constants.TotalViewports),
+            new TableItem(MethodOffset.VertexBufferDrawState,  typeof(VertexBufferDrawState),  1),
+            new TableItem(MethodOffset.DepthBiasState,         typeof(DepthBiasState),         1),
+            new TableItem(MethodOffset.ScissorState,           typeof(ScissorState),           Constants.TotalViewports),
+            new TableItem(MethodOffset.StencilBackMasks,       typeof(StencilBackMasks),       1),
+            new TableItem(MethodOffset.RtDepthStencilState,    typeof(RtDepthStencilState),    1),
+            new TableItem(MethodOffset.VertexAttribState,      typeof(VertexAttribState),      Constants.TotalVertexAttribs),
+            new TableItem(MethodOffset.RtDepthStencilSize,     typeof(Size3D),                 1),
+            new TableItem(MethodOffset.BlendEnable,            typeof(Boolean32),              Constants.TotalRenderTargets),
+            new TableItem(MethodOffset.StencilTestState,       typeof(StencilTestState),       1),
+            new TableItem(MethodOffset.SamplerPoolState,       typeof(PoolState),              1),
+            new TableItem(MethodOffset.TexturePoolState,       typeof(PoolState),              1),
+            new TableItem(MethodOffset.StencilBackTestState,   typeof(StencilBackTestState),   1),
+            new TableItem(MethodOffset.ShaderBaseAddress,      typeof(GpuVa),                  1),
+            new TableItem(MethodOffset.PrimitiveRestartState,  typeof(PrimitiveRestartState),  1),
+            new TableItem(MethodOffset.IndexBufferState,       typeof(IndexBufferState),       1),
+            new TableItem(MethodOffset.VertexBufferInstanced,  typeof(Boolean32),              Constants.TotalVertexBuffers),
+            new TableItem(MethodOffset.FaceState,              typeof(FaceState),              1),
+            new TableItem(MethodOffset.RtColorMask,            typeof(RtColorMask),            Constants.TotalRenderTargets),
+            new TableItem(MethodOffset.VertexBufferState,      typeof(VertexBufferState),      Constants.TotalVertexBuffers),
+            new TableItem(MethodOffset.BlendConstant,          typeof(ColorF),                 1),
+            new TableItem(MethodOffset.BlendState,             typeof(BlendState),             Constants.TotalRenderTargets),
+            new TableItem(MethodOffset.VertexBufferEndAddress, typeof(GpuVa),                  Constants.TotalVertexBuffers),
+            new TableItem(MethodOffset.ShaderState,            typeof(ShaderState),            Constants.ShaderStages + 1),
         };
     }
 }
\ No newline at end of file
diff --git a/Ryujinx.Graphics.Gpu/State/MethodOffset.cs b/Ryujinx.Graphics.Gpu/State/MethodOffset.cs
index d7d5d90391..b0eb6f328e 100644
--- a/Ryujinx.Graphics.Gpu/State/MethodOffset.cs
+++ b/Ryujinx.Graphics.Gpu/State/MethodOffset.cs
@@ -28,10 +28,13 @@ namespace Ryujinx.Graphics.Gpu.State
         SyncpointAction                 = 0xb2,
         CopyBuffer                      = 0xc0,
         RasterizeEnable                 = 0xdf,
+        TfBufferState                   = 0xe0,
         CopyBufferParams                = 0x100,
+        TfState                         = 0x1c0,
         CopyBufferSwizzle               = 0x1c2,
         CopyBufferDstTexture            = 0x1c3,
         CopyBufferSrcTexture            = 0x1ca,
+        TfEnable                        = 0x1d1,
         RtColorState                    = 0x200,
         CopyTextureControl              = 0x223,
         CopyRegion                      = 0x22c,
@@ -116,6 +119,7 @@ namespace Ryujinx.Graphics.Gpu.State
         UniformBufferBindTessEvaluation = 0x914,
         UniformBufferBindGeometry       = 0x91c,
         UniformBufferBindFragment       = 0x924,
-        TextureBufferIndex              = 0x982
+        TextureBufferIndex              = 0x982,
+        TfVaryingLocations              = 0xa00
     }
 }
\ No newline at end of file
diff --git a/Ryujinx.Graphics.Gpu/State/ReportCounterType.cs b/Ryujinx.Graphics.Gpu/State/ReportCounterType.cs
index cface55d03..6bde284415 100644
--- a/Ryujinx.Graphics.Gpu/State/ReportCounterType.cs
+++ b/Ryujinx.Graphics.Gpu/State/ReportCounterType.cs
@@ -11,18 +11,19 @@ namespace Ryujinx.Graphics.Gpu.State
         VertexShaderInvocations            = 5,
         GeometryShaderInvocations          = 7,
         GeometryShaderPrimitives           = 9,
+        ZcullStats0                        = 0xa,
         TransformFeedbackPrimitivesWritten = 0xb,
+        ZcullStats1                        = 0xc,
+        ZcullStats2                        = 0xe,
         ClipperInputPrimitives             = 0xf,
+        ZcullStats3                        = 0x10,
         ClipperOutputPrimitives            = 0x11,
         PrimitivesGenerated                = 0x12,
         FragmentShaderInvocations          = 0x13,
         SamplesPassed                      = 0x15,
+        TransformFeedbackOffset            = 0x1a,
         TessControlShaderInvocations       = 0x1b,
         TessEvaluationShaderInvocations    = 0x1d,
-        TessEvaluationShaderPrimitives     = 0x1f,
-        ZcullStats0                        = 0x2a,
-        ZcullStats1                        = 0x2c,
-        ZcullStats2                        = 0x2e,
-        ZcullStats3                        = 0x30
+        TessEvaluationShaderPrimitives     = 0x1f
     }
 }
\ No newline at end of file
diff --git a/Ryujinx.Graphics.Gpu/State/TfBufferState.cs b/Ryujinx.Graphics.Gpu/State/TfBufferState.cs
new file mode 100644
index 0000000000..24dc0952fe
--- /dev/null
+++ b/Ryujinx.Graphics.Gpu/State/TfBufferState.cs
@@ -0,0 +1,18 @@
+namespace Ryujinx.Graphics.Gpu.State
+{
+    /// <summary>
+    /// Transform feedback buffer state.
+    /// </summary>
+    struct TfBufferState
+    {
+#pragma warning disable CS0649
+        public Boolean32 Enable;
+        public GpuVa     Address;
+        public int       Size;
+        public int       Offset;
+        public uint      Padding0;
+        public uint      Padding1;
+        public uint      Padding2;
+#pragma warning restore CS0649
+    }
+}
diff --git a/Ryujinx.Graphics.Gpu/State/TfState.cs b/Ryujinx.Graphics.Gpu/State/TfState.cs
new file mode 100644
index 0000000000..fb8b950bcf
--- /dev/null
+++ b/Ryujinx.Graphics.Gpu/State/TfState.cs
@@ -0,0 +1,15 @@
+namespace Ryujinx.Graphics.Gpu.State
+{
+    /// <summary>
+    /// Transform feedback state.
+    /// </summary>
+    struct TfState
+    {
+#pragma warning disable CS0649
+        public int BufferIndex;
+        public int VaryingsCount;
+        public int Stride;
+        public uint Padding;
+#pragma warning restore CS0649
+    }
+}
diff --git a/Ryujinx.Graphics.OpenGL/EnumConversion.cs b/Ryujinx.Graphics.OpenGL/EnumConversion.cs
index a4bd39ccaf..09860be3e1 100644
--- a/Ryujinx.Graphics.OpenGL/EnumConversion.cs
+++ b/Ryujinx.Graphics.OpenGL/EnumConversion.cs
@@ -331,6 +331,31 @@ namespace Ryujinx.Graphics.OpenGL
             return PrimitiveType.Points;
         }
 
+        public static TransformFeedbackPrimitiveType ConvertToTfType(this PrimitiveTopology topology)
+        {
+            switch (topology)
+            {
+                case PrimitiveTopology.Points:
+                    return TransformFeedbackPrimitiveType.Points;
+                case PrimitiveTopology.Lines:
+                case PrimitiveTopology.LineLoop:
+                case PrimitiveTopology.LineStrip:
+                case PrimitiveTopology.LinesAdjacency:
+                case PrimitiveTopology.LineStripAdjacency:
+                    return TransformFeedbackPrimitiveType.Lines;
+                case PrimitiveTopology.Triangles:
+                case PrimitiveTopology.TriangleStrip:
+                case PrimitiveTopology.TriangleFan:
+                case PrimitiveTopology.TrianglesAdjacency:
+                case PrimitiveTopology.TriangleStripAdjacency:
+                    return TransformFeedbackPrimitiveType.Triangles;
+            }
+
+            Logger.PrintDebug(LogClass.Gpu, $"Invalid {nameof(PrimitiveTopology)} enum value: {topology}.");
+
+            return TransformFeedbackPrimitiveType.Points;
+        }
+
         public static OpenTK.Graphics.OpenGL.StencilOp Convert(this GAL.StencilOp op)
         {
             switch (op)
diff --git a/Ryujinx.Graphics.OpenGL/Pipeline.cs b/Ryujinx.Graphics.OpenGL/Pipeline.cs
index 9623c826f8..7537b44fab 100644
--- a/Ryujinx.Graphics.OpenGL/Pipeline.cs
+++ b/Ryujinx.Graphics.OpenGL/Pipeline.cs
@@ -45,6 +45,8 @@ namespace Ryujinx.Graphics.OpenGL
 
         private bool _scissor0Enable = false;
 
+        private bool _tfEnabled;
+
         ColorF _blendConstant = new ColorF(0, 0, 0, 0);
 
         internal Pipeline()
@@ -76,6 +78,12 @@ namespace Ryujinx.Graphics.OpenGL
             GL.MemoryBarrier(MemoryBarrierFlags.AllBarrierBits);
         }
 
+        public void BeginTransformFeedback(PrimitiveTopology topology)
+        {
+            GL.BeginTransformFeedback(topology.ConvertToTfType());
+            _tfEnabled = true;
+        }
+
         public void ClearRenderTargetColor(int index, uint componentMask, ColorF color)
         {
             GL.ColorMask(
@@ -512,6 +520,12 @@ namespace Ryujinx.Graphics.OpenGL
             }
         }
 
+        public void EndTransformFeedback()
+        {
+            GL.EndTransformFeedback();
+            _tfEnabled = false;
+        }
+
         public void SetBlendState(int index, BlendDescriptor blend)
         {
             if (!blend.Enable)
@@ -713,7 +727,17 @@ namespace Ryujinx.Graphics.OpenGL
         public void SetProgram(IProgram program)
         {
             _program = (Program)program;
-            _program.Bind();
+
+            if (_tfEnabled)
+            {
+                GL.PauseTransformFeedback();
+                _program.Bind();
+                GL.ResumeTransformFeedback();
+            }
+            else
+            {
+                _program.Bind();
+            }
 
             SetRenderTargetScale(_fpRenderScale[0]);
         }
@@ -904,6 +928,22 @@ namespace Ryujinx.Graphics.OpenGL
             }
         }
 
+        public void SetTransformFeedbackBuffer(int index, BufferRange buffer)
+        {
+            const BufferRangeTarget target = BufferRangeTarget.TransformFeedbackBuffer;
+
+            if (_tfEnabled)
+            {
+                GL.PauseTransformFeedback();
+                GL.BindBufferRange(target, index, buffer.Handle.ToInt32(), (IntPtr)buffer.Offset, buffer.Size);
+                GL.ResumeTransformFeedback();
+            }
+            else
+            {
+                GL.BindBufferRange(target, index, buffer.Handle.ToInt32(), (IntPtr)buffer.Offset, buffer.Size);
+            }
+        }
+
         public void SetUniformBuffer(int index, ShaderStage stage, BufferRange buffer)
         {
             SetBuffer(index, stage, buffer, isStorage: false);
@@ -1132,7 +1172,7 @@ namespace Ryujinx.Graphics.OpenGL
                 {
                     // If the event has been flushed, then just use the values on the CPU.
                     // The query object may already be repurposed for another draw (eg. begin + end).
-                    return false; 
+                    return false;
                 }
 
                 if (compare == 0 && evt.Type == QueryTarget.SamplesPassed && evt.ClearCounter)
@@ -1145,7 +1185,7 @@ namespace Ryujinx.Graphics.OpenGL
             // The GPU will flush the queries to CPU and evaluate the condition there instead.
 
             GL.Flush(); // The thread will be stalled manually flushing the counter, so flush GL commands now.
-            return false; 
+            return false;
         }
 
         public bool TryHostConditionalRendering(ICounterEvent value, ICounterEvent compare, bool isEqual)
diff --git a/Ryujinx.Graphics.OpenGL/Program.cs b/Ryujinx.Graphics.OpenGL/Program.cs
index 8b4f6e242a..a0f8eb0159 100644
--- a/Ryujinx.Graphics.OpenGL/Program.cs
+++ b/Ryujinx.Graphics.OpenGL/Program.cs
@@ -2,6 +2,10 @@ using OpenTK.Graphics.OpenGL;
 using Ryujinx.Common.Logging;
 using Ryujinx.Graphics.GAL;
 using Ryujinx.Graphics.Shader;
+using Ryujinx.Graphics.Shader.CodeGen.Glsl;
+using System;
+using System.Collections.Generic;
+using System.Linq;
 
 namespace Ryujinx.Graphics.OpenGL
 {
@@ -31,7 +35,7 @@ namespace Ryujinx.Graphics.OpenGL
         private int[] _textureUnits;
         private int[] _imageUnits;
 
-        public Program(IShader[] shaders)
+        public Program(IShader[] shaders, TransformFeedbackDescriptor[] transformFeedbackDescriptors)
         {
             _ubBindingPoints = new int[UbsPerStage  * ShaderStages];
             _sbBindingPoints = new int[SbsPerStage  * ShaderStages];
@@ -67,6 +71,54 @@ namespace Ryujinx.Graphics.OpenGL
                 GL.AttachShader(Handle, shaderHandle);
             }
 
+            if (transformFeedbackDescriptors != null)
+            {
+                List<string> varyings = new List<string>();
+
+                int cbi = 0;
+
+                foreach (var tfd in transformFeedbackDescriptors.OrderBy(x => x.BufferIndex))
+                {
+                    if (tfd.VaryingLocations.Length == 0)
+                    {
+                        continue;
+                    }
+
+                    while (cbi < tfd.BufferIndex)
+                    {
+                        varyings.Add("gl_NextBuffer");
+
+                        cbi++;
+                    }
+
+                    int stride = Math.Min(128 * 4, (tfd.Stride + 3) & ~3);
+
+                    int j = 0;
+
+                    for (; j < tfd.VaryingLocations.Length && j * 4 < stride; j++)
+                    {
+                        byte location = tfd.VaryingLocations[j];
+
+                        varyings.Add(Varying.GetName(location) ?? "gl_SkipComponents1");
+
+                        j += Varying.GetSize(location) - 1;
+                    }
+
+                    int feedbackBytes = j * 4;
+
+                    while (feedbackBytes < stride)
+                    {
+                        int bytes = Math.Min(16, stride - feedbackBytes);
+
+                        varyings.Add($"gl_SkipComponents{(bytes / 4)}");
+
+                        feedbackBytes += bytes;
+                    }
+                }
+
+                GL.TransformFeedbackVaryings(Handle, varyings.Count, varyings.ToArray(), TransformFeedbackMode.InterleavedAttribs);
+            }
+
             GL.LinkProgram(Handle);
 
             for (int index = 0; index < shaders.Length; index++)
diff --git a/Ryujinx.Graphics.OpenGL/Renderer.cs b/Ryujinx.Graphics.OpenGL/Renderer.cs
index cf90f81f50..49324637bc 100644
--- a/Ryujinx.Graphics.OpenGL/Renderer.cs
+++ b/Ryujinx.Graphics.OpenGL/Renderer.cs
@@ -44,9 +44,9 @@ namespace Ryujinx.Graphics.OpenGL
             return Buffer.Create(size);
         }
 
-        public IProgram CreateProgram(IShader[] shaders)
+        public IProgram CreateProgram(IShader[] shaders, TransformFeedbackDescriptor[] transformFeedbackDescriptors)
         {
-            return new Program(shaders);
+            return new Program(shaders, transformFeedbackDescriptors);
         }
 
         public ISampler CreateSampler(SamplerCreateInfo info)
diff --git a/Ryujinx.Graphics.Shader/CodeGen/Glsl/Declarations.cs b/Ryujinx.Graphics.Shader/CodeGen/Glsl/Declarations.cs
index f9d619286a..efd30143ba 100644
--- a/Ryujinx.Graphics.Shader/CodeGen/Glsl/Declarations.cs
+++ b/Ryujinx.Graphics.Shader/CodeGen/Glsl/Declarations.cs
@@ -15,7 +15,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
 
         public static void Declare(CodeGenContext context, StructuredProgramInfo info)
         {
-            context.AppendLine("#version 430 core");
+            context.AppendLine("#version 440 core");
             context.AppendLine("#extension GL_ARB_gpu_shader_int64 : enable");
             context.AppendLine("#extension GL_ARB_shader_ballot : enable");
             context.AppendLine("#extension GL_ARB_shader_group_vote : enable");
@@ -234,7 +234,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
                 string stage = OperandManager.GetShaderStagePrefix(context.Config.Stage);
 
                 int scaleElements = context.TextureDescriptors.Count;
-                
+
                 if (context.Config.Stage == ShaderStage.Fragment)
                 {
                     scaleElements++; // Also includes render target scale, for gl_FragCoord.
@@ -424,7 +424,12 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
                     };
                 }
 
-                context.AppendLine($"layout (location = {attr}) {iq}in vec4 {DefaultNames.IAttributePrefix}{attr}{suffix};");
+                for (int c = 0; c < 4; c++)
+                {
+                    char swzMask = "xyzw"[c];
+
+                    context.AppendLine($"layout (location = {attr}, component = {c}) {iq}in float {DefaultNames.IAttributePrefix}{attr}_{swzMask}{suffix};");
+                }
             }
         }
 
@@ -452,12 +457,22 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
         {
             for (int attr = 0; attr < MaxAttributes; attr++)
             {
-                context.AppendLine($"layout (location = {attr}) out vec4 {DefaultNames.OAttributePrefix}{attr};");
+                for (int c = 0; c < 4; c++)
+                {
+                    char swzMask = "xyzw"[c];
+
+                    context.AppendLine($"layout (location = {attr}, component = {c}) out float {DefaultNames.OAttributePrefix}{attr}_{swzMask};");
+                }
             }
 
             foreach (int attr in info.OAttributes.OrderBy(x => x).Where(x => x >= MaxAttributes))
             {
-                context.AppendLine($"layout (location = {attr}) out vec4 {DefaultNames.OAttributePrefix}{attr};");
+                for (int c = 0; c < 4; c++)
+                {
+                    char swzMask = "xyzw"[c];
+
+                    context.AppendLine($"layout (location = {attr}, component = {c}) out float {DefaultNames.OAttributePrefix}{attr}_{swzMask};");
+                }
             }
         }
 
diff --git a/Ryujinx.Graphics.Shader/CodeGen/Glsl/GlslGenerator.cs b/Ryujinx.Graphics.Shader/CodeGen/Glsl/GlslGenerator.cs
index 1465338e1e..2105560afc 100644
--- a/Ryujinx.Graphics.Shader/CodeGen/Glsl/GlslGenerator.cs
+++ b/Ryujinx.Graphics.Shader/CodeGen/Glsl/GlslGenerator.cs
@@ -56,7 +56,10 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
                         continue;
                     }
 
-                    context.AppendLine($"{DefaultNames.OAttributePrefix}{attr} = vec4(0);");
+                    context.AppendLine($"{DefaultNames.OAttributePrefix}{attr}_x = 0;");
+                    context.AppendLine($"{DefaultNames.OAttributePrefix}{attr}_y = 0;");
+                    context.AppendLine($"{DefaultNames.OAttributePrefix}{attr}_z = 0;");
+                    context.AppendLine($"{DefaultNames.OAttributePrefix}{attr}_w = 0;");
                 }
             }
 
diff --git a/Ryujinx.Graphics.Shader/CodeGen/Glsl/OperandManager.cs b/Ryujinx.Graphics.Shader/CodeGen/Glsl/OperandManager.cs
index 971284f4c5..8801fc1113 100644
--- a/Ryujinx.Graphics.Shader/CodeGen/Glsl/OperandManager.cs
+++ b/Ryujinx.Graphics.Shader/CodeGen/Glsl/OperandManager.cs
@@ -147,7 +147,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
         {
             int value = attr.Value;
 
-            string swzMask = GetSwizzleMask((value >> 2) & 3);
+            char swzMask = GetSwizzleMask((value >> 2) & 3);
 
             if (value >= AttributeConsts.UserAttributeBase &&
                 value <  AttributeConsts.UserAttributeEnd)
@@ -158,15 +158,13 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
                     ? DefaultNames.OAttributePrefix
                     : DefaultNames.IAttributePrefix;
 
-                string name = $"{prefix}{(value >> 4)}";
+                string name = $"{prefix}{(value >> 4)}_{swzMask}";
 
                 if (stage == ShaderStage.Geometry && !isOutAttr)
                 {
                     name += $"[{indexExpr}]";
                 }
 
-                name += "." + swzMask;
-
                 return name;
             }
             else
@@ -264,9 +262,9 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
             return _stagePrefixes[index];
         }
 
-        private static string GetSwizzleMask(int value)
+        private static char GetSwizzleMask(int value)
         {
-            return "xyzw".Substring(value, 1);
+            return "xyzw"[value];
         }
 
         public static VariableType GetNodeDestType(IAstNode node)
diff --git a/Ryujinx.Graphics.Shader/CodeGen/Glsl/Varying.cs b/Ryujinx.Graphics.Shader/CodeGen/Glsl/Varying.cs
new file mode 100644
index 0000000000..b9b2afb476
--- /dev/null
+++ b/Ryujinx.Graphics.Shader/CodeGen/Glsl/Varying.cs
@@ -0,0 +1,69 @@
+using Ryujinx.Graphics.Shader.Translation;
+
+namespace Ryujinx.Graphics.Shader.CodeGen.Glsl
+{
+    public static class Varying
+    {
+        public static string GetName(int offset)
+        {
+            offset <<= 2;
+
+            if (offset >= AttributeConsts.UserAttributeBase &&
+                offset <  AttributeConsts.UserAttributeEnd)
+            {
+                offset -= AttributeConsts.UserAttributeBase;
+
+                string name = $"{ DefaultNames.OAttributePrefix}{(offset >> 4)}";
+
+                name += "_" + "xyzw"[(offset >> 2) & 3];
+
+                return name;
+            }
+
+            switch (offset)
+            {
+                case AttributeConsts.PositionX:
+                case AttributeConsts.PositionY:
+                case AttributeConsts.PositionZ:
+                case AttributeConsts.PositionW:
+                    return "gl_Position";
+                case AttributeConsts.PointSize:
+                    return "gl_PointSize";
+                case AttributeConsts.ClipDistance0:
+                    return "gl_ClipDistance[0]";
+                case AttributeConsts.ClipDistance1:
+                    return "gl_ClipDistance[1]";
+                case AttributeConsts.ClipDistance2:
+                    return "gl_ClipDistance[2]";
+                case AttributeConsts.ClipDistance3:
+                    return "gl_ClipDistance[3]";
+                case AttributeConsts.ClipDistance4:
+                    return "gl_ClipDistance[4]";
+                case AttributeConsts.ClipDistance5:
+                    return "gl_ClipDistance[5]";
+                case AttributeConsts.ClipDistance6:
+                    return "gl_ClipDistance[6]";
+                case AttributeConsts.ClipDistance7:
+                    return "gl_ClipDistance[7]";
+                case AttributeConsts.VertexId:
+                    return "gl_VertexID";
+            }
+
+            return null;
+        }
+
+        public static int GetSize(int offset)
+        {
+            switch (offset << 2)
+            {
+                case AttributeConsts.PositionX:
+                case AttributeConsts.PositionY:
+                case AttributeConsts.PositionZ:
+                case AttributeConsts.PositionW:
+                    return 4;
+            }
+
+            return 1;
+        }
+    }
+}
\ No newline at end of file