diff --git a/src/video_core/renderer_metal/mtl_buffer_cache.cpp b/src/video_core/renderer_metal/mtl_buffer_cache.cpp
index dfde651144..4b2a051829 100644
--- a/src/video_core/renderer_metal/mtl_buffer_cache.cpp
+++ b/src/video_core/renderer_metal/mtl_buffer_cache.cpp
@@ -22,15 +22,6 @@ MTL::Buffer* CreatePrivateBuffer(const Device& device, size_t size) {
 
 } // Anonymous namespace
 
-BoundBuffer::BoundBuffer(MTL::Buffer* buffer_, size_t offset_, size_t size_)
-    : buffer{buffer_->retain()}, offset{offset_}, size{size_} {}
-
-BoundBuffer::~BoundBuffer() {
-    if (buffer) {
-        buffer->release();
-    }
-}
-
 BufferView::BufferView(MTL::Buffer* buffer_, size_t offset_, size_t size_,
                        VideoCore::Surface::PixelFormat format_)
     : buffer{buffer_->retain()}, offset{offset_}, size{size_}, format{format_} {}
@@ -94,24 +85,29 @@ void BufferCacheRuntime::ClearBuffer(MTL::Buffer* dest_buffer, u32 offset, size_
 void BufferCacheRuntime::BindIndexBuffer(PrimitiveTopology topology, IndexFormat index_format,
                                          u32 base_vertex, u32 num_indices, MTL::Buffer* buffer,
                                          u32 offset, [[maybe_unused]] u32 size) {
-    // TODO: convert parameters to Metal enums
-    bound_index_buffer = {BoundBuffer(buffer, offset, size)};
+    command_recorder.SetIndexBuffer(buffer, offset, index_format, topology, num_indices,
+                                    base_vertex);
 }
 
 void BufferCacheRuntime::BindQuadIndexBuffer(PrimitiveTopology topology, u32 first, u32 count) {
     // TODO: bind quad index buffer
 }
 
-void BufferCacheRuntime::BindVertexBuffer(u32 index, MTL::Buffer* buffer, u32 offset, u32 size,
-                                          u32 stride) {
+void BufferCacheRuntime::BindVertexBuffer(size_t stage, u32 index, MTL::Buffer* buffer, u32 offset,
+                                          u32 size, u32 stride) {
     // TODO: use stride
-    bound_vertex_buffers[MAX_METAL_BUFFERS - index - 1] = {BoundBuffer(buffer, offset, size)};
+    BindBuffer(stage, MAX_METAL_BUFFERS - index - 1, buffer, offset, size);
 }
 
 void BufferCacheRuntime::BindVertexBuffers(VideoCommon::HostBindings<Buffer>& bindings) {
     // TODO: implement
 }
 
+void BufferCacheRuntime::BindBuffer(size_t stage, u32 binding_index, MTL::Buffer* buffer,
+                                    u32 offset, u32 size) {
+    command_recorder.SetBuffer(stage, buffer, binding_index, offset);
+}
+
 void BufferCacheRuntime::ReserveNullBuffer() {
     if (!null_buffer) {
         null_buffer = CreateNullBuffer();
diff --git a/src/video_core/renderer_metal/mtl_buffer_cache.h b/src/video_core/renderer_metal/mtl_buffer_cache.h
index 0fda327b35..1037863ae3 100644
--- a/src/video_core/renderer_metal/mtl_buffer_cache.h
+++ b/src/video_core/renderer_metal/mtl_buffer_cache.h
@@ -17,17 +17,6 @@ class CommandRecorder;
 
 class BufferCacheRuntime;
 
-struct BoundBuffer {
-    BoundBuffer() = default;
-    BoundBuffer(MTL::Buffer* buffer_, size_t offset_, size_t size_);
-
-    ~BoundBuffer();
-
-    MTL::Buffer* buffer = nil;
-    size_t offset{};
-    size_t size{};
-};
-
 struct BufferView {
     BufferView(MTL::Buffer* buffer_, size_t offset_, size_t size_,
                VideoCore::Surface::PixelFormat format_ = VideoCore::Surface::PixelFormat::Invalid);
@@ -121,7 +110,8 @@ public:
 
     void BindQuadIndexBuffer(PrimitiveTopology topology, u32 first, u32 count);
 
-    void BindVertexBuffer(u32 index, MTL::Buffer* buffer, u32 offset, u32 size, u32 stride);
+    void BindVertexBuffer(size_t stage, u32 index, MTL::Buffer* buffer, u32 offset, u32 size,
+                          u32 stride);
 
     void BindVertexBuffers(VideoCommon::HostBindings<Buffer>& bindings);
 
@@ -131,31 +121,35 @@ public:
     // TODO: implement
     void BindTransformFeedbackBuffers(VideoCommon::HostBindings<Buffer>& bindings) {}
 
-    std::span<u8> BindMappedUniformBuffer([[maybe_unused]] size_t stage,
-                                          [[maybe_unused]] u32 binding_index, u32 size) {
+    std::span<u8> BindMappedUniformBuffer(size_t stage, u32 binding_index, u32 size) {
         const StagingBufferRef ref = staging_pool.Request(size, MemoryUsage::Upload);
-        BindBuffer(ref.buffer, static_cast<u32>(ref.offset), size);
+        BindBuffer(stage, binding_index, ref.buffer, static_cast<u32>(ref.offset), size);
         return ref.mapped_span;
     }
 
-    void BindUniformBuffer(MTL::Buffer* buffer, u32 offset, u32 size) {
-        BindBuffer(buffer, offset, size);
+    void BindUniformBuffer(size_t stage, u32 binding_index, MTL::Buffer* buffer, u32 offset,
+                           u32 size) {
+        BindBuffer(stage, binding_index, buffer, offset, size);
     }
 
-    void BindStorageBuffer(MTL::Buffer* buffer, u32 offset, u32 size,
-                           [[maybe_unused]] bool is_written) {
-        BindBuffer(buffer, offset, size);
+    // TODO: implement
+    void BindComputeUniformBuffer(u32 binding_index, MTL::Buffer* buffer, u32 offset, u32 size) {}
+
+    void BindStorageBuffer(size_t stage, u32 binding_index, MTL::Buffer* buffer, u32 offset,
+                           u32 size, [[maybe_unused]] bool is_written) {
+        BindBuffer(stage, binding_index, buffer, offset, size);
     }
 
+    // TODO: implement
+    void BindComputeStorageBuffer(u32 binding_index, Buffer& buffer, u32 offset, u32 size,
+                                  bool is_written) {}
+
     // TODO: implement
     void BindTextureBuffer(Buffer& buffer, u32 offset, u32 size,
                            VideoCore::Surface::PixelFormat format) {}
 
 private:
-    void BindBuffer(MTL::Buffer* buffer, u32 offset, u32 size) {
-        // FIXME: what should be the index?
-        bound_buffers[0] = BoundBuffer(buffer, offset, size);
-    }
+    void BindBuffer(size_t stage, u32 binding_index, MTL::Buffer* buffer, u32 offset, u32 size);
 
     void ReserveNullBuffer();
     MTL::Buffer* CreateNullBuffer();
@@ -167,17 +161,6 @@ private:
     // Common buffers
     MTL::Buffer* null_buffer = nil;
     MTL::Buffer* quad_index_buffer = nil;
-
-    // TODO: probably move this into a separate class
-    // Bound state
-    // Vertex buffers are bound to MAX_METAL_BUFFERS - index - 1, while regular buffers are bound to
-    // index
-    BoundBuffer bound_vertex_buffers[MAX_METAL_BUFFERS] = {{}};
-    struct {
-        BoundBuffer buffer;
-        // TODO: include index type and primitive topology
-    } bound_index_buffer = {};
-    BoundBuffer bound_buffers[MAX_METAL_BUFFERS] = {{}};
 };
 
 struct BufferCacheParams {
@@ -189,8 +172,8 @@ struct BufferCacheParams {
     static constexpr bool IS_OPENGL = false;
     static constexpr bool HAS_PERSISTENT_UNIFORM_BUFFER_BINDINGS = false;
     static constexpr bool HAS_FULL_INDEX_AND_PRIMITIVE_SUPPORT = false;
-    static constexpr bool NEEDS_BIND_UNIFORM_INDEX = false;
-    static constexpr bool NEEDS_BIND_STORAGE_INDEX = false;
+    static constexpr bool NEEDS_BIND_UNIFORM_INDEX = true;
+    static constexpr bool NEEDS_BIND_STORAGE_INDEX = true;
     static constexpr bool USE_MEMORY_MAPS = true;
     static constexpr bool SEPARATE_IMAGE_BUFFER_BINDINGS = false;
     static constexpr bool USE_MEMORY_MAPS_FOR_UPLOADS = true;
diff --git a/src/video_core/renderer_metal/mtl_command_recorder.cpp b/src/video_core/renderer_metal/mtl_command_recorder.cpp
index 81cca08361..701c9d3b03 100644
--- a/src/video_core/renderer_metal/mtl_command_recorder.cpp
+++ b/src/video_core/renderer_metal/mtl_command_recorder.cpp
@@ -11,12 +11,86 @@ CommandRecorder::CommandRecorder(const Device& device_) : device(device_) {}
 CommandRecorder::~CommandRecorder() = default;
 
 void CommandRecorder::BeginOrContinueRenderPass(MTL::RenderPassDescriptor* render_pass) {
+    bool should_reset_bound_resources = false;
     if (render_pass != render_state.render_pass) {
-        RequireCommandBuffer();
         EndEncoding();
+        RequireCommandBuffer();
         encoder = command_buffer->renderCommandEncoder(render_pass);
         encoder_type = EncoderType::Render;
         render_state.render_pass = render_pass;
+        should_reset_bound_resources = true;
+    }
+    const auto bind_resources{[&](size_t stage) {
+        // Buffers
+        for (u8 i = 0; i < MAX_BUFFERS; i++) {
+            auto& bound_buffer = render_state.buffers[stage][i];
+            if (bound_buffer.buffer &&
+                (bound_buffer.needs_update || should_reset_bound_resources)) {
+                switch (stage) {
+                case 0:
+                    GetRenderCommandEncoderUnchecked()->setVertexBuffer(bound_buffer.buffer, i,
+                                                                        bound_buffer.offset);
+                    break;
+                case 4:
+                    GetRenderCommandEncoderUnchecked()->setFragmentBuffer(bound_buffer.buffer, i,
+                                                                          bound_buffer.offset);
+                    break;
+                }
+                bound_buffer.needs_update = false;
+            }
+        }
+        // Textures
+        for (u8 i = 0; i < MAX_TEXTURES; i++) {
+            auto& bound_texture = render_state.textures[stage][i];
+            if (bound_texture.texture &&
+                (bound_texture.needs_update || should_reset_bound_resources)) {
+                switch (stage) {
+                case 0:
+                    GetRenderCommandEncoderUnchecked()->setVertexTexture(bound_texture.texture, i);
+                    break;
+                case 4:
+                    GetRenderCommandEncoderUnchecked()->setFragmentTexture(bound_texture.texture,
+                                                                           i);
+                    break;
+                }
+                bound_texture.needs_update = false;
+            }
+        }
+        // Sampler states
+        for (u8 i = 0; i < MAX_SAMPLERS; i++) {
+            auto& bound_sampler_state = render_state.sampler_states[stage][i];
+            if (bound_sampler_state.sampler_state &&
+                (bound_sampler_state.needs_update || should_reset_bound_resources)) {
+                switch (stage) {
+                case 0:
+                    GetRenderCommandEncoderUnchecked()->setVertexSamplerState(
+                        bound_sampler_state.sampler_state, i);
+                    break;
+                case 4:
+                    GetRenderCommandEncoderUnchecked()->setFragmentSamplerState(
+                        bound_sampler_state.sampler_state, i);
+                    break;
+                }
+                bound_sampler_state.needs_update = false;
+            }
+        }
+    }};
+
+    bind_resources(0);
+    bind_resources(4);
+
+    if (should_reset_bound_resources) {
+        for (size_t stage = 0; stage < 5; stage++) {
+            for (u8 i = 0; i < MAX_BUFFERS; i++) {
+                render_state.buffers[stage][i].buffer = nullptr;
+            }
+            for (u8 i = 0; i < MAX_TEXTURES; i++) {
+                render_state.textures[stage][i].texture = nullptr;
+            }
+            for (u8 i = 0; i < MAX_SAMPLERS; i++) {
+                render_state.sampler_states[stage][i].sampler_state = nullptr;
+            }
+        }
     }
 }
 
@@ -44,7 +118,8 @@ void CommandRecorder::EndEncoding() {
         //[encoder release];
         encoder = nullptr;
         if (encoder_type == EncoderType::Render) {
-            render_state = {};
+            render_state.render_pass = nullptr;
+            render_state.pipeline_state = nullptr;
         }
     }
 }
diff --git a/src/video_core/renderer_metal/mtl_command_recorder.h b/src/video_core/renderer_metal/mtl_command_recorder.h
index feb81d10aa..50058f93ab 100644
--- a/src/video_core/renderer_metal/mtl_command_recorder.h
+++ b/src/video_core/renderer_metal/mtl_command_recorder.h
@@ -6,6 +6,8 @@
 #include <Metal/Metal.hpp>
 #include <QuartzCore/QuartzCore.hpp>
 
+#include "video_core/engines/maxwell_3d.h"
+
 namespace Metal {
 
 class Device;
@@ -16,27 +18,48 @@ constexpr size_t MAX_BUFFERS = 31;
 constexpr size_t MAX_TEXTURES = 31;
 constexpr size_t MAX_SAMPLERS = 31;
 
+struct BoundBuffer {
+    bool needs_update{true};
+    MTL::Buffer* buffer{nullptr};
+    size_t offset{0};
+};
+
+struct BoundTexture {
+    bool needs_update{true};
+    MTL::Texture* texture{nullptr};
+};
+
+struct BoundSamplerState {
+    bool needs_update{true};
+    MTL::SamplerState* sampler_state{nullptr};
+};
+
+struct BoundIndexBuffer {
+    MTL::Buffer* buffer{nullptr};
+    size_t offset{0};
+    MTL::IndexType index_format;
+    MTL::PrimitiveType primitive_topology;
+    u32 num_indices;
+    u32 base_vertex;
+};
+
 struct RenderState {
     MTL::RenderPassDescriptor* render_pass{nullptr};
     MTL::RenderPipelineState* pipeline_state{nullptr};
 
-    MTL::Buffer* vertex_buffers[MAX_BUFFERS] = {nullptr};
-    MTL::Buffer* fragment_buffers[MAX_BUFFERS] = {nullptr};
-    MTL::Buffer* compute_buffers[MAX_BUFFERS] = {nullptr};
-
-    MTL::Texture* vertex_textures[MAX_TEXTURES] = {nullptr};
-    MTL::Texture* fragment_textures[MAX_TEXTURES] = {nullptr};
-    MTL::Texture* compute_textures[MAX_TEXTURES] = {nullptr};
-
-    MTL::SamplerState* vertex_sampler_states[MAX_SAMPLERS] = {nullptr};
-    MTL::SamplerState* fragment_sampler_states[MAX_SAMPLERS] = {nullptr};
-    MTL::SamplerState* compute_sampler_states[MAX_SAMPLERS] = {nullptr};
+    BoundBuffer buffers[5][MAX_BUFFERS] = {{}};
+    BoundTexture textures[5][MAX_TEXTURES] = {{}};
+    BoundSamplerState sampler_states[5][MAX_SAMPLERS] = {{}};
+    BoundIndexBuffer bound_index_buffer;
 };
 
 // TODO: whenever a render pass gets interrupted by either a compute or blit command and application
 // then tries to perform a render command, begin the same render pass, but with all load actions set
 // to "load"
 class CommandRecorder {
+    using PrimitiveTopology = Tegra::Engines::Maxwell3D::Regs::PrimitiveTopology;
+    using IndexFormat = Tegra::Engines::Maxwell3D::Regs::IndexFormat;
+
 public:
     CommandRecorder(const Device& device_);
     ~CommandRecorder();
@@ -64,10 +87,14 @@ public:
         return command_buffer;
     }
 
+    MTL::RenderCommandEncoder* GetRenderCommandEncoderUnchecked() {
+        return static_cast<MTL::RenderCommandEncoder*>(encoder);
+    }
+
     MTL::RenderCommandEncoder* GetRenderCommandEncoder() {
         CheckIfRenderPassIsActive();
 
-        return static_cast<MTL::RenderCommandEncoder*>(encoder);
+        return GetRenderCommandEncoderUnchecked();
     }
 
     MTL::ComputeCommandEncoder* GetComputeCommandEncoder() {
@@ -90,67 +117,34 @@ public:
         }
     }
 
-    inline void SetVertexBuffer(MTL::Buffer* buffer, size_t index) {
-        if (buffer != render_state.vertex_buffers[index]) {
-            GetRenderCommandEncoder()->setVertexBuffer(buffer, index, 0);
-            render_state.vertex_buffers[index] = buffer;
+    inline void SetBuffer(size_t stage, MTL::Buffer* buffer, size_t index, size_t offset) {
+        auto& bound_buffer = render_state.buffers[stage][index];
+        if (buffer != bound_buffer.buffer) {
+            bound_buffer = {true, buffer, offset};
         }
     }
 
-    inline void SetFragmentBuffer(MTL::Buffer* buffer, size_t index) {
-        if (buffer != render_state.fragment_buffers[index]) {
-            GetRenderCommandEncoder()->setFragmentBuffer(buffer, index, 0);
-            render_state.fragment_buffers[index] = buffer;
+    inline void SetTexture(size_t stage, MTL::Texture* texture, size_t index) {
+        auto& bound_texture = render_state.textures[stage][index];
+        if (texture != bound_texture.texture) {
+            bound_texture = {true, texture};
         }
     }
 
-    inline void SetComputeBuffer(MTL::Buffer* buffer, size_t index) {
-        if (buffer != render_state.compute_buffers[index]) {
-            GetComputeCommandEncoder()->setBuffer(buffer, index, 0);
-            render_state.compute_buffers[index] = buffer;
+    inline void SetSamplerState(size_t stage, MTL::SamplerState* sampler_state, size_t index) {
+        auto& bound_sampler_state = render_state.sampler_states[stage][index];
+        if (sampler_state != bound_sampler_state.sampler_state) {
+            bound_sampler_state = {true, sampler_state};
         }
     }
 
-    inline void SetVertexTexture(MTL::Texture* texture, size_t index) {
-        if (texture != render_state.vertex_textures[index]) {
-            GetRenderCommandEncoder()->setVertexTexture(texture, index);
-            render_state.vertex_textures[index] = texture;
-        }
-    }
-
-    inline void SetFragmentTexture(MTL::Texture* texture, size_t index) {
-        if (texture != render_state.fragment_textures[index]) {
-            GetRenderCommandEncoder()->setFragmentTexture(texture, index);
-            render_state.fragment_textures[index] = texture;
-        }
-    }
-
-    inline void SetComputeTexture(MTL::Texture* texture, size_t index) {
-        if (texture != render_state.compute_textures[index]) {
-            GetComputeCommandEncoder()->setTexture(texture, index);
-            render_state.compute_textures[index] = texture;
-        }
-    }
-
-    inline void SetVertexSamplerState(MTL::SamplerState* sampler_state, size_t index) {
-        if (sampler_state != render_state.vertex_sampler_states[index]) {
-            GetRenderCommandEncoder()->setVertexSamplerState(sampler_state, index);
-            render_state.vertex_sampler_states[index] = sampler_state;
-        }
-    }
-
-    inline void SetFragmentSamplerState(MTL::SamplerState* sampler_state, size_t index) {
-        if (sampler_state != render_state.fragment_sampler_states[index]) {
-            GetRenderCommandEncoder()->setFragmentSamplerState(sampler_state, index);
-            render_state.fragment_sampler_states[index] = sampler_state;
-        }
-    }
-
-    inline void SetComputeSamplerState(MTL::SamplerState* sampler_state, size_t index) {
-        if (sampler_state != render_state.compute_sampler_states[index]) {
-            GetComputeCommandEncoder()->setSamplerState(sampler_state, index);
-            render_state.compute_sampler_states[index] = sampler_state;
-        }
+    inline void SetIndexBuffer(MTL::Buffer* buffer, size_t offset, IndexFormat index_format,
+                               PrimitiveTopology primitive_topology, u32 num_indices,
+                               u32 base_vertex) {
+        // TODO: convert parameters to Metal enums
+        render_state.bound_index_buffer = {
+            buffer,      offset,     MTL::IndexTypeUInt32, MTL::PrimitiveTypeTriangle,
+            num_indices, base_vertex};
     }
 
 private:
diff --git a/src/video_core/renderer_metal/mtl_graphics_pipeline.cpp b/src/video_core/renderer_metal/mtl_graphics_pipeline.cpp
index 91716d085a..700221d46a 100644
--- a/src/video_core/renderer_metal/mtl_graphics_pipeline.cpp
+++ b/src/video_core/renderer_metal/mtl_graphics_pipeline.cpp
@@ -58,70 +58,146 @@ GraphicsPipeline::GraphicsPipeline(const Device& device_, CommandRecorder& comma
 }
 
 void GraphicsPipeline::Configure(bool is_indexed) {
-    buffer_cache.UpdateGraphicsBuffers(is_indexed);
-    buffer_cache.BindHostGeometryBuffers(is_indexed);
-
     texture_cache.SynchronizeGraphicsDescriptors();
 
-    // Find resources
-    size_t stage = 4;
-    const auto& cbufs{maxwell3d->state.shader_stages[stage].const_buffers};
-    const auto read_handle{[&](const auto& desc, u32 index) {
-        ASSERT(cbufs[desc.cbuf_index].enabled);
-        const u32 index_offset{index << desc.size_shift};
-        const u32 offset{desc.cbuf_offset + index_offset};
-        const GPUVAddr addr{cbufs[desc.cbuf_index].address + offset};
-        if constexpr (std::is_same_v<decltype(desc), const Shader::TextureDescriptor&> ||
-                      std::is_same_v<decltype(desc), const Shader::TextureBufferDescriptor&>) {
-            if (desc.has_secondary) {
-                ASSERT(cbufs[desc.secondary_cbuf_index].enabled);
-                const u32 second_offset{desc.secondary_cbuf_offset + index_offset};
-                const GPUVAddr separate_addr{cbufs[desc.secondary_cbuf_index].address +
-                                             second_offset};
-                const u32 lhs_raw{gpu_memory->Read<u32>(addr) << desc.shift_left};
-                const u32 rhs_raw{gpu_memory->Read<u32>(separate_addr)
-                                  << desc.secondary_shift_left};
-                const u32 raw{lhs_raw | rhs_raw};
-
-                return TexturePair(raw, false);
-            }
-        }
-        auto a = gpu_memory->Read<u32>(addr);
-        // HACK: this particular texture breaks SMO
-        if (a == 310378931)
-            a = 310378932;
-
-        return TexturePair(a, false);
-    }};
-
-    const Shader::Info& info{stage_infos[stage]};
-
     std::array<VideoCommon::ImageViewInOut, 32> views;
     std::array<VideoCommon::SamplerId, 32> samplers;
     size_t view_index{};
     size_t sampler_index{};
 
-    for (const auto& desc : info.texture_descriptors) {
-        for (u32 index = 0; index < desc.count; ++index) {
-            const auto handle{read_handle(desc, index)};
-            views[view_index++] = {handle.first};
-
-            VideoCommon::SamplerId sampler{texture_cache.GetGraphicsSamplerId(handle.second)};
-            samplers[sampler_index++] = sampler;
+    // Find resources
+    const auto& regs{maxwell3d->regs};
+    const bool via_header_index{regs.sampler_binding == Maxwell::SamplerBinding::ViaHeaderBinding};
+    const auto configure_stage{[&](u32 stage) {
+        const Shader::Info& info{stage_infos[stage]};
+        buffer_cache.UnbindGraphicsStorageBuffers(stage);
+        size_t ssbo_index{};
+        for (const auto& desc : info.storage_buffers_descriptors) {
+            ASSERT(desc.count == 1);
+            buffer_cache.BindGraphicsStorageBuffer(stage, ssbo_index, desc.cbuf_index,
+                                                   desc.cbuf_offset, desc.is_written);
+            ++ssbo_index;
         }
-    }
+        const auto& cbufs{maxwell3d->state.shader_stages[stage].const_buffers};
+        const auto read_handle{[&](const auto& desc, u32 index) {
+            ASSERT(cbufs[desc.cbuf_index].enabled);
+            const u32 index_offset{index << desc.size_shift};
+            const u32 offset{desc.cbuf_offset + index_offset};
+            const GPUVAddr addr{cbufs[desc.cbuf_index].address + offset};
+            if constexpr (std::is_same_v<decltype(desc), const Shader::TextureDescriptor&> ||
+                          std::is_same_v<decltype(desc), const Shader::TextureBufferDescriptor&>) {
+                if (desc.has_secondary) {
+                    ASSERT(cbufs[desc.secondary_cbuf_index].enabled);
+                    const u32 second_offset{desc.secondary_cbuf_offset + index_offset};
+                    const GPUVAddr separate_addr{cbufs[desc.secondary_cbuf_index].address +
+                                                 second_offset};
+                    const u32 lhs_raw{gpu_memory->Read<u32>(addr) << desc.shift_left};
+                    const u32 rhs_raw{gpu_memory->Read<u32>(separate_addr)
+                                      << desc.secondary_shift_left};
+                    const u32 raw{lhs_raw | rhs_raw};
+                    return TexturePair(raw, via_header_index);
+                }
+            }
+            auto a = gpu_memory->Read<u32>(addr);
+            // HACK: this particular texture breaks SMO
+            if (a == 310378931)
+                a = 310378932;
+
+            return TexturePair(a, via_header_index);
+        }};
+        const auto add_image{[&](const auto& desc, bool blacklist) {
+            for (u32 index = 0; index < desc.count; ++index) {
+                const auto handle{read_handle(desc, index)};
+                views[view_index++] = {
+                    .index = handle.first,
+                    .blacklist = blacklist,
+                    .id = {},
+                };
+            }
+        }};
+        /*
+        if constexpr (Spec::has_texture_buffers) {
+            for (const auto& desc : info.texture_buffer_descriptors) {
+                add_image(desc, false);
+            }
+        }
+        */
+        /*
+        if constexpr (Spec::has_image_buffers) {
+            for (const auto& desc : info.image_buffer_descriptors) {
+                add_image(desc, false);
+            }
+        }
+        */
+        for (const auto& desc : info.texture_descriptors) {
+            for (u32 index = 0; index < desc.count; ++index) {
+                const auto handle{read_handle(desc, index)};
+                views[view_index++] = {handle.first};
+
+                VideoCommon::SamplerId sampler{texture_cache.GetGraphicsSamplerId(handle.second)};
+                samplers[sampler_index++] = sampler;
+            }
+        }
+        for (const auto& desc : info.image_descriptors) {
+            add_image(desc, desc.is_written);
+        }
+        /*
+        const auto& cbufs{maxwell3d->state.shader_stages[stage].const_buffers};
+        const auto read_handle{[&](const auto& desc, u32 index) {
+            ASSERT(cbufs[desc.cbuf_index].enabled);
+            const u32 index_offset{index << desc.size_shift};
+            const u32 offset{desc.cbuf_offset + index_offset};
+            const GPUVAddr addr{cbufs[desc.cbuf_index].address + offset};
+            if constexpr (std::is_same_v<decltype(desc), const Shader::TextureDescriptor&> ||
+                          std::is_same_v<decltype(desc), const Shader::TextureBufferDescriptor&>) {
+                if (desc.has_secondary) {
+                    ASSERT(cbufs[desc.secondary_cbuf_index].enabled);
+                    const u32 second_offset{desc.secondary_cbuf_offset + index_offset};
+                    const GPUVAddr separate_addr{cbufs[desc.secondary_cbuf_index].address +
+                                                 second_offset};
+                    const u32 lhs_raw{gpu_memory->Read<u32>(addr) << desc.shift_left};
+                    const u32 rhs_raw{gpu_memory->Read<u32>(separate_addr)
+                                      << desc.secondary_shift_left};
+                    const u32 raw{lhs_raw | rhs_raw};
+
+                    return TexturePair(raw, false);
+                }
+            }
+            auto a = gpu_memory->Read<u32>(addr);
+            // HACK: this particular texture breaks SMO
+            if (a == 310378931)
+                a = 310378932;
+
+            return TexturePair(a, false);
+        }};
+
+        const Shader::Info& info{stage_infos[stage]};
+
+        std::array<VideoCommon::ImageViewInOut, 32> views;
+        std::array<VideoCommon::SamplerId, 32> samplers;
+        size_t view_index{};
+        size_t sampler_index{};
+
+        for (const auto& desc : info.texture_descriptors) {
+            for (u32 index = 0; index < desc.count; ++index) {
+                const auto handle{read_handle(desc, index)};
+                views[view_index++] = {handle.first};
+
+                VideoCommon::SamplerId sampler{texture_cache.GetGraphicsSamplerId(handle.second)};
+                samplers[sampler_index++] = sampler;
+            }
+        }
+        */
+    }};
+
+    configure_stage(0);
+    configure_stage(4);
+
+    buffer_cache.UpdateGraphicsBuffers(is_indexed);
+    buffer_cache.BindHostGeometryBuffers(is_indexed);
+
     texture_cache.FillGraphicsImageViews<true>(std::span(views.data(), view_index));
 
-    // Begin render pass
-    texture_cache.UpdateRenderTargets(false);
-    const Framebuffer* const framebuffer = texture_cache.GetFramebuffer();
-    if (!framebuffer) {
-        return;
-    }
-    command_recorder.BeginOrContinueRenderPass(framebuffer->GetHandle());
-
-    command_recorder.SetRenderPipelineState(pipeline_state);
-
     // Bind resources
 
     // HACK: try to find a texture that we can bind
@@ -131,8 +207,18 @@ void GraphicsPipeline::Configure(bool is_indexed) {
     ImageView& image_view{texture_cache.GetImageView(views_it->id)};
     Sampler& sampler{texture_cache.GetSampler(*samplers_it)};
 
-    command_recorder.SetFragmentTexture(image_view.GetHandle(), 0);
-    command_recorder.SetFragmentSamplerState(sampler.GetHandle(), 0);
+    command_recorder.SetTexture(4, image_view.GetHandle(), 0);
+    command_recorder.SetSamplerState(4, sampler.GetHandle(), 0);
+
+    // Begin render pass
+    texture_cache.UpdateRenderTargets(false);
+    const Framebuffer* const framebuffer = texture_cache.GetFramebuffer();
+    if (!framebuffer) {
+        return;
+    }
+    command_recorder.BeginOrContinueRenderPass(framebuffer->GetHandle());
+
+    command_recorder.SetRenderPipelineState(pipeline_state);
 }
 
 void GraphicsPipeline::MakePipeline(MTL::RenderPassDescriptor* render_pass) {
diff --git a/src/video_core/renderer_metal/mtl_pipeline_cache.cpp b/src/video_core/renderer_metal/mtl_pipeline_cache.cpp
index 012b58e017..3276bde04f 100644
--- a/src/video_core/renderer_metal/mtl_pipeline_cache.cpp
+++ b/src/video_core/renderer_metal/mtl_pipeline_cache.cpp
@@ -272,7 +272,7 @@ std::unique_ptr<GraphicsPipeline> PipelineCache::CreateGraphicsPipeline(
         std::cout << code << std::endl;
         MTL::CompileOptions* compile_options = MTL::CompileOptions::alloc()->init();
         NS::Error* error = nullptr;
-        MTL::Library* library = device.GetDevice()->newLibrary(
+        [[maybe_unused]] MTL::Library* library = device.GetDevice()->newLibrary(
             NS::String::string(code.c_str(), NS::ASCIIStringEncoding), compile_options, &error);
         if (error) {
             LOG_ERROR(Render_Metal, "failed to create library: {}",
@@ -280,11 +280,11 @@ std::unique_ptr<GraphicsPipeline> PipelineCache::CreateGraphicsPipeline(
             // HACK
             std::cout << error->description()->cString(NS::ASCIIStringEncoding) << std::endl;
             // HACK
-            throw;
+            // throw;
         }
 
-        functions[index - 1] =
-            library->newFunction(NS::String::string("main_", NS::ASCIIStringEncoding));
+        // functions[stage_index] =
+        //     library->newFunction(NS::String::string("main_", NS::ASCIIStringEncoding));
         previous_stage = &program;
     }
 
@@ -316,8 +316,8 @@ std::unique_ptr<GraphicsPipeline> PipelineCache::CreateGraphicsPipeline(
             return out;
         }
 
-        fragment float4 fragmentMain(VertexOut in [[stage_in]], texture2d<float> tex [[texture(0)]], sampler samplr [[sampler(0)]]) {
-            return tex.sample(samplr, in.texCoord);
+        fragment float4 fragmentMain(VertexOut in [[stage_in]], texture2d<float> tex [[texture(0)]],
+    sampler samplr [[sampler(0)]]) { return tex.sample(samplr, in.texCoord);
         }
     )",
                                                                NS::ASCIIStringEncoding),
diff --git a/src/video_core/renderer_metal/renderer_metal.cpp b/src/video_core/renderer_metal/renderer_metal.cpp
index efb83743c6..036e5c29a9 100644
--- a/src/video_core/renderer_metal/renderer_metal.cpp
+++ b/src/video_core/renderer_metal/renderer_metal.cpp
@@ -42,18 +42,20 @@ void RendererMetal::Composite(std::span<const Tegra::FramebufferConfig> framebuf
     render_pass_descriptor->colorAttachments()->object(0)->setTexture(
         swap_chain.GetDrawableTexture());
 
-    command_recorder.BeginOrContinueRenderPass(render_pass_descriptor);
-
-    // Blit the framebuffer to the drawable texture
+    // Bind the source texture
     // TODO: acquire the texture from @ref framebuffers
     const Framebuffer* const framebuffer = rasterizer.texture_cache.GetFramebuffer();
     if (!framebuffer) {
         return;
     }
     MTL::Texture* src_texture = framebuffer->GetHandle()->colorAttachments()->object(0)->texture();
+    command_recorder.SetTexture(4, src_texture, 0);
+    command_recorder.SetSamplerState(4, blit_sampler_state, 0);
+
+    command_recorder.BeginOrContinueRenderPass(render_pass_descriptor);
+
+    // Blit the framebuffer to the drawable texture
     command_recorder.SetRenderPipelineState(blit_pipeline_state);
-    command_recorder.SetFragmentTexture(src_texture, 0);
-    command_recorder.SetFragmentSamplerState(blit_sampler_state, 0);
 
     // Draw a full screen triangle which will get clipped to a rectangle
     command_recorder.GetRenderCommandEncoder()->drawPrimitives(MTL::PrimitiveTypeTriangle,