diff --git a/src/video_core/renderer_metal/mtl_graphics_pipeline.cpp b/src/video_core/renderer_metal/mtl_graphics_pipeline.cpp
index 819e327897..cfe827c100 100644
--- a/src/video_core/renderer_metal/mtl_graphics_pipeline.cpp
+++ b/src/video_core/renderer_metal/mtl_graphics_pipeline.cpp
@@ -63,18 +63,7 @@ void GraphicsPipeline::Configure(bool is_indexed) {
 
     texture_cache.SynchronizeGraphicsDescriptors();
 
-    texture_cache.UpdateRenderTargets(false);
-    const Framebuffer* const framebuffer = texture_cache.GetFramebuffer();
-    if (!framebuffer) {
-        return;
-    }
-    command_recorder.BeginOrContinueRenderPass(framebuffer->GetHandle());
-
-    command_recorder.GetRenderCommandEncoder()->setRenderPipelineState(pipeline_state);
-
-    // Bind resources
-
-    // HACK: try to find a texture that we can bind
+    // 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) {
@@ -116,6 +105,20 @@ void GraphicsPipeline::Configure(bool 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.GetRenderCommandEncoder()->setRenderPipelineState(pipeline_state);
+
+    // Bind resources
+
+    // HACK: try to find a texture that we can bind
     VideoCommon::ImageViewInOut* texture_buffer_it{views.data()};
 
     ImageView& image_view{texture_cache.GetImageView(texture_buffer_it->id)};
diff --git a/src/video_core/renderer_metal/mtl_texture_cache.cpp b/src/video_core/renderer_metal/mtl_texture_cache.cpp
index 9d87a23df2..cf37577595 100644
--- a/src/video_core/renderer_metal/mtl_texture_cache.cpp
+++ b/src/video_core/renderer_metal/mtl_texture_cache.cpp
@@ -11,6 +11,7 @@
 #include "common/bit_util.h"
 #include "common/settings.h"
 
+#include "video_core/renderer_metal/mtl_command_recorder.h"
 #include "video_core/renderer_metal/mtl_device.h"
 #include "video_core/renderer_metal/mtl_texture_cache.h"
 
@@ -53,16 +54,16 @@ void TextureCacheRuntime::FreeDeferredStagingBuffer(StagingBufferRef& ref) {
     staging_buffer_pool.FreeDeferred(ref);
 }
 
-Image::Image(TextureCacheRuntime& runtime, const ImageInfo& info, GPUVAddr gpu_addr_,
+Image::Image(TextureCacheRuntime& runtime_, const ImageInfo& info, GPUVAddr gpu_addr_,
              VAddr cpu_addr_)
-    : VideoCommon::ImageBase(info, gpu_addr_, cpu_addr_) {
+    : VideoCommon::ImageBase(info, gpu_addr_, cpu_addr_), runtime{&runtime_} {
     MTL::TextureDescriptor* texture_descriptor = MTL::TextureDescriptor::alloc()->init();
     // TODO: don't hardcode the format
     texture_descriptor->setPixelFormat(MTL::PixelFormatRGBA8Unorm);
     texture_descriptor->setWidth(info.size.width);
     texture_descriptor->setHeight(info.size.height);
 
-    texture = runtime.device.GetDevice()->newTexture(texture_descriptor);
+    texture = runtime->device.GetDevice()->newTexture(texture_descriptor);
 }
 
 Image::Image(const VideoCommon::NullImageParams& params) : VideoCommon::ImageBase{params} {}
@@ -76,12 +77,22 @@ Image::~Image() {
 // TODO: implement these
 void Image::UploadMemory(MTL::Buffer* buffer, size_t offset,
                          std::span<const VideoCommon::BufferImageCopy> copies) {
-    ;
+    for (const VideoCommon::BufferImageCopy& copy : copies) {
+        // TODO: query this from texture format
+        size_t bytes_per_pixel = 4;
+        size_t bytes_per_row = info.size.width * bytes_per_pixel;
+        size_t bytes_per_image = info.size.height * bytes_per_row;
+        MTL::Size size = MTL::Size::Make(info.size.width, info.size.height, 1);
+        MTL::Origin origin = MTL::Origin::Make(copy.image_offset.x, copy.image_offset.y,
+                                               copy.image_subresource.base_layer);
+        runtime->command_recorder.GetBlitCommandEncoder()->copyFromBuffer(
+            buffer, offset, bytes_per_row, bytes_per_image, size, texture, 0, 0, origin);
+    }
 }
 
 void Image::UploadMemory(const StagingBufferRef& map,
                          std::span<const VideoCommon::BufferImageCopy> copies) {
-    ;
+    UploadMemory(map.buffer, map.offset, copies);
 }
 
 void Image::DownloadMemory(MTL::Buffer* buffer, size_t offset,
diff --git a/src/video_core/renderer_metal/mtl_texture_cache.h b/src/video_core/renderer_metal/mtl_texture_cache.h
index 509c5561fd..799a993e5d 100644
--- a/src/video_core/renderer_metal/mtl_texture_cache.h
+++ b/src/video_core/renderer_metal/mtl_texture_cache.h
@@ -116,7 +116,7 @@ public:
 
 class Image : public VideoCommon::ImageBase {
 public:
-    explicit Image(TextureCacheRuntime& runtime, const VideoCommon::ImageInfo& info,
+    explicit Image(TextureCacheRuntime& runtime_, const VideoCommon::ImageInfo& info,
                    GPUVAddr gpu_addr, VAddr cpu_addr);
     explicit Image(const VideoCommon::NullImageParams&);
 
@@ -162,7 +162,9 @@ public:
     }
 
 private:
-    MTL::Texture* texture = nil;
+    TextureCacheRuntime* runtime;
+
+    MTL::Texture* texture{nullptr};
     bool initialized = false;
 
     bool rescaled = false;