SDL: Support extended interop with the 2D Metal renderer

From fe85b4782d520f443cda247a19ecc1c4409ecdcc Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Thu, 30 Jul 2026 16:38:00 -0700
Subject: [PATCH] Support extended interop with the 2D Metal renderer

Also fixed an issue where an external surface couldn't be used with IYUV and YV12 textures.
---
 include/SDL3/SDL_render.h                     |   54 +
 src/render/metal/SDL_render_metal.m           |  161 +-
 src/render/metal/SDL_shaders_metal.metal      |    7 +-
 src/render/metal/SDL_shaders_metal_ios.h      | 2243 +++++++-------
 .../metal/SDL_shaders_metal_iphonesimulator.h | 2686 ++++++++---------
 src/render/metal/SDL_shaders_metal_macos.h    | 2077 +++++++------
 src/render/metal/SDL_shaders_metal_tvos.h     | 2243 +++++++-------
 .../metal/SDL_shaders_metal_tvsimulator.h     | 2615 ++++++++--------
 8 files changed, 6074 insertions(+), 6012 deletions(-)

diff --git a/include/SDL3/SDL_render.h b/include/SDL3/SDL_render.h
index ee32cf912b9a6..8c48a5aea8da3 100644
--- a/include/SDL3/SDL_render.h
+++ b/include/SDL3/SDL_render.h
@@ -306,6 +306,13 @@ extern SDL_DECLSPEC SDL_Renderer * SDLCALL SDL_CreateRenderer(SDL_Window *window
  * - `SDL_PROP_RENDERER_CREATE_GPU_SHADERS_MSL_BOOLEAN`: the app is able to
  *   provide MSL shaders to SDL_GPURenderState, optional.
  *
+ * With the metal renderer:
+ *
+ * - `SDL_PROP_RENDERER_CREATE_METAL_DEVICE_POINTER`: the MTLDevice to use with the
+ *   renderer, optional.
+ * - `SDL_PROP_RENDERER_CREATE_METAL_COMMAND_QUEUE_POINTER`: the MTLCommandQueue
+ *   to use with the renderer, optional. If you set this property it will implicitly set (and override) `SDL_PROP_RENDERER_CREATE_METAL_DEVICE_POINTER`.
+ *
  * With the vulkan renderer:
  *
  * - `SDL_PROP_RENDERER_CREATE_VULKAN_INSTANCE_POINTER`: the VkInstance to use
@@ -346,6 +353,8 @@ extern SDL_DECLSPEC SDL_Renderer * SDLCALL SDL_CreateRendererWithProperties(SDL_
 #define SDL_PROP_RENDERER_CREATE_GPU_SHADERS_SPIRV_BOOLEAN                  "SDL.renderer.create.gpu.shaders_spirv"
 #define SDL_PROP_RENDERER_CREATE_GPU_SHADERS_DXIL_BOOLEAN                   "SDL.renderer.create.gpu.shaders_dxil"
 #define SDL_PROP_RENDERER_CREATE_GPU_SHADERS_MSL_BOOLEAN                    "SDL.renderer.create.gpu.shaders_msl"
+#define SDL_PROP_RENDERER_CREATE_METAL_DEVICE_POINTER                       "SDL.renderer.create.metal.device"
+#define SDL_PROP_RENDERER_CREATE_METAL_COMMAND_QUEUE_POINTER                "SDL.renderer.create.metal.command_queue"
 #define SDL_PROP_RENDERER_CREATE_VULKAN_INSTANCE_POINTER                    "SDL.renderer.create.vulkan.instance"
 #define SDL_PROP_RENDERER_CREATE_VULKAN_SURFACE_NUMBER                      "SDL.renderer.create.vulkan.surface"
 #define SDL_PROP_RENDERER_CREATE_VULKAN_PHYSICAL_DEVICE_POINTER             "SDL.renderer.create.vulkan.physical_device"
@@ -518,6 +527,15 @@ extern SDL_DECLSPEC const char * SDLCALL SDL_GetRendererName(SDL_Renderer *rende
  * - `SDL_PROP_RENDERER_D3D12_COMMAND_QUEUE_POINTER`: the ID3D12CommandQueue
  *   associated with the renderer
  *
+ * With the metal renderer:
+ *
+ * - `SDL_PROP_RENDERER_METAL_DEVICE_POINTER`: the MTLDevice associated with the
+ *   renderer
+ * - `SDL_PROP_RENDERER_METAL_COMMAND_QUEUE_POINTER`: the MTLCommandQueue
+ *   associated with the renderer. Work submitted on this queue will be ordered
+ *   relative to other rendering. SDL_FlushRenderer() can be used to guarantee
+ *   the current rendering has been submitted.
+ *
  * With the vulkan renderer:
  *
  * - `SDL_PROP_RENDERER_VULKAN_INSTANCE_POINTER`: the VkInstance associated
@@ -568,6 +586,8 @@ extern SDL_DECLSPEC SDL_PropertiesID SDLCALL SDL_GetRendererProperties(SDL_Rende
 #define SDL_PROP_RENDERER_D3D12_DEVICE_POINTER                      "SDL.renderer.d3d12.device"
 #define SDL_PROP_RENDERER_D3D12_SWAPCHAIN_POINTER                   "SDL.renderer.d3d12.swap_chain"
 #define SDL_PROP_RENDERER_D3D12_COMMAND_QUEUE_POINTER               "SDL.renderer.d3d12.command_queue"
+#define SDL_PROP_RENDERER_METAL_DEVICE_POINTER                      "SDL.renderer.metal.device"
+#define SDL_PROP_RENDERER_METAL_COMMAND_QUEUE_POINTER               "SDL.renderer.metal.command_queue"
 #define SDL_PROP_RENDERER_VULKAN_INSTANCE_POINTER                   "SDL.renderer.vulkan.instance"
 #define SDL_PROP_RENDERER_VULKAN_SURFACE_NUMBER                     "SDL.renderer.vulkan.surface"
 #define SDL_PROP_RENDERER_VULKAN_PHYSICAL_DEVICE_POINTER            "SDL.renderer.vulkan.physical_device"
@@ -736,6 +756,19 @@ extern SDL_DECLSPEC SDL_Texture * SDLCALL SDL_CreateTextureFromSurface(SDL_Rende
  * - `SDL_PROP_TEXTURE_CREATE_METAL_PIXELBUFFER_POINTER`: the CVPixelBufferRef
  *   associated with the texture, if you want to create a texture from an
  *   existing pixel buffer.
+ * - `SDL_PROP_TEXTURE_CREATE_METAL_TEXTURE_POINTER`: the MTLTexture
+ *   associated with the texture, if you want to wrap an existing texture.
+ * - `SDL_PROP_TEXTURE_CREATE_METAL_TEXTURE_UV_POINTER`: the MTLTexture
+ *   associated with the UV plane of an NV12 texture, if you want to wrap an
+ *   existing texture.
+ * - `SDL_PROP_TEXTURE_CREATE_METAL_TEXTURE_U_POINTER`: the MTLTexture
+ *   associated with the U plane of a YUV texture, if you want to wrap an
+ *   existing texture.
+ * - `SDL_PROP_TEXTURE_CREATE_METAL_TEXTURE_V_POINTER`: the MTLTexture
+ *   associated with the V plane of a YUV texture, if you want to wrap an
+ *   existing texture.
+ * - `SDL_PROP_TEXTURE_CREATE_METAL_TEXTURE_USAGE_NUMBER`: any additional
+ *   MTLTextureUsage that this texture should have, defaults to 0.
  *
  * With the opengl renderer:
  *
@@ -819,6 +852,11 @@ extern SDL_DECLSPEC SDL_Texture * SDLCALL SDL_CreateTextureWithProperties(SDL_Re
 #define SDL_PROP_TEXTURE_CREATE_D3D12_TEXTURE_U_POINTER         "SDL.texture.create.d3d12.texture_u"
 #define SDL_PROP_TEXTURE_CREATE_D3D12_TEXTURE_V_POINTER         "SDL.texture.create.d3d12.texture_v"
 #define SDL_PROP_TEXTURE_CREATE_METAL_PIXELBUFFER_POINTER       "SDL.texture.create.metal.pixelbuffer"
+#define SDL_PROP_TEXTURE_CREATE_METAL_TEXTURE_POINTER           "SDL.texture.create.metal.texture"
+#define SDL_PROP_TEXTURE_CREATE_METAL_TEXTURE_UV_POINTER        "SDL.texture.create.metal.texture_uv"
+#define SDL_PROP_TEXTURE_CREATE_METAL_TEXTURE_U_POINTER         "SDL.texture.create.metal.texture_u"
+#define SDL_PROP_TEXTURE_CREATE_METAL_TEXTURE_V_POINTER         "SDL.texture.create.metal.texture_v"
+#define SDL_PROP_TEXTURE_CREATE_METAL_TEXTURE_USAGE_NUMBER      "SDL.texture.create.metal.texture_usage"
 #define SDL_PROP_TEXTURE_CREATE_OPENGL_TEXTURE_NUMBER           "SDL.texture.create.opengl.texture"
 #define SDL_PROP_TEXTURE_CREATE_OPENGL_TEXTURE_UV_NUMBER        "SDL.texture.create.opengl.texture_uv"
 #define SDL_PROP_TEXTURE_CREATE_OPENGL_TEXTURE_U_NUMBER         "SDL.texture.create.opengl.texture_u"
@@ -877,6 +915,18 @@ extern SDL_DECLSPEC SDL_Texture * SDLCALL SDL_CreateTextureWithProperties(SDL_Re
  * - `SDL_PROP_TEXTURE_D3D12_TEXTURE_V_POINTER`: the ID3D12Resource associated
  *   with the V plane of a YUV texture
  *
+ * With the metal renderer:
+ *
+ * - `SDL_PROP_TEXTURE_METAL_TEXTURE_POINTER`: the MTLTexture associated with
+ *   the texture
+ * - `SDL_PROP_TEXTURE_METAL_TEXTURE_UV_POINTER`: the MTLTexture associated
+ *   with the UV plane of an NV12 texture
+ * - `SDL_PROP_TEXTURE_METAL_TEXTURE_U_POINTER`: the MTLTexture associated with
+ *   the U plane of a YUV texture
+ *   style texture
+ * - `SDL_PROP_TEXTURE_METAL_TEXTURE_V_POINTER`: the MTLTexture associated with
+ *   the V plane of a YUV texture
+ *
  * With the vulkan renderer:
  *
  * - `SDL_PROP_TEXTURE_VULKAN_TEXTURE_NUMBER`: the VkImage associated with the
@@ -946,6 +996,10 @@ extern SDL_DECLSPEC SDL_PropertiesID SDLCALL SDL_GetTextureProperties(SDL_Textur
 #define SDL_PROP_TEXTURE_D3D12_TEXTURE_POINTER              "SDL.texture.d3d12.texture"
 #define SDL_PROP_TEXTURE_D3D12_TEXTURE_U_POINTER            "SDL.texture.d3d12.texture_u"
 #define SDL_PROP_TEXTURE_D3D12_TEXTURE_V_POINTER            "SDL.texture.d3d12.texture_v"
+#define SDL_PROP_TEXTURE_METAL_TEXTURE_POINTER              "SDL.texture.metal.texture"
+#define SDL_PROP_TEXTURE_METAL_TEXTURE_UV_POINTER           "SDL.texture.metal.texture_uv"
+#define SDL_PROP_TEXTURE_METAL_TEXTURE_U_POINTER            "SDL.texture.metal.texture_u"
+#define SDL_PROP_TEXTURE_METAL_TEXTURE_V_POINTER            "SDL.texture.metal.texture_v"
 #define SDL_PROP_TEXTURE_OPENGL_TEXTURE_NUMBER              "SDL.texture.opengl.texture"
 #define SDL_PROP_TEXTURE_OPENGL_TEXTURE_UV_NUMBER           "SDL.texture.opengl.texture_uv"
 #define SDL_PROP_TEXTURE_OPENGL_TEXTURE_U_NUMBER            "SDL.texture.opengl.texture_u"
diff --git a/src/render/metal/SDL_render_metal.m b/src/render/metal/SDL_render_metal.m
index 15a0634e29304..332c1ca06281a 100644
--- a/src/render/metal/SDL_render_metal.m
+++ b/src/render/metal/SDL_render_metal.m
@@ -161,7 +161,9 @@ @implementation SDL3METAL_PaletteData
 @interface SDL3METAL_TextureData : NSObject
 @property(nonatomic, retain) id<MTLTexture> mtltexture;
 @property(nonatomic, retain) id<MTLTexture> mtlpalette;
-@property(nonatomic, retain) id<MTLTexture> mtltextureUv;
+@property(nonatomic, retain) id<MTLTexture> mtltextureUV;
+@property(nonatomic, retain) id<MTLTexture> mtltextureU;
+@property(nonatomic, retain) id<MTLTexture> mtltextureV;
 @property(nonatomic, assign) SDL_MetalFragmentFunction fragmentFunction;
 #ifdef SDL_HAVE_YUV
 @property(nonatomic, assign) BOOL yuv;
@@ -699,8 +701,8 @@ static bool METAL_CreateTexture(SDL_Renderer *renderer, SDL_Texture *texture, SD
         SDL3METAL_RenderData *data = (__bridge SDL3METAL_RenderData *)renderer->internal;
         MTLPixelFormat pixfmt = MTLPixelFormatInvalid;
         MTLTextureDescriptor *mtltexdesc;
-        id<MTLTexture> mtltexture = nil, mtltextureUv = nil;
-        SDL3METAL_TextureData *texturedata;
+        id<MTLTexture> mtltexture = nil;
+        SDL3METAL_TextureData *texturedata = [[SDL3METAL_TextureData alloc] init];
         CVPixelBufferRef pixelbuffer = nil;
         IOSurfaceRef surface = nil;
 
@@ -786,23 +788,24 @@ static bool METAL_CreateTexture(SDL_Renderer *renderer, SDL_Texture *texture, SD
         } else {
             mtltexdesc.usage = MTLTextureUsageShaderRead;
         }
+        mtltexdesc.usage |= SDL_GetNumberProperty(create_props, SDL_PROP_TEXTURE_CREATE_METAL_TEXTURE_USAGE_NUMBER, 0);
 
-        if (surface) {
-            mtltexture = [data.mtldevice newTextureWithDescriptor:mtltexdesc iosurface:surface plane:0];
-        } else {
+        mtltexture = (__bridge id<MTLTexture>)SDL_GetPointerProperty(create_props, SDL_PROP_TEXTURE_CREATE_METAL_TEXTURE_POINTER, nil);
+        if (mtltexture == nil) {
             mtltexture = [data.mtldevice newTextureWithDescriptor:mtltexdesc];
         }
         if (mtltexture == nil) {
             return SDL_SetError("Texture allocation failed");
         }
+        texturedata.mtltexture = mtltexture;
+        SDL_SetPointerProperty(SDL_GetTextureProperties(texture), SDL_PROP_TEXTURE_METAL_TEXTURE_POINTER, (__bridge void *)mtltexture);
 
-        mtltextureUv = nil;
 #ifdef SDL_HAVE_YUV
         BOOL yuv = (texture->format == SDL_PIXELFORMAT_IYUV || texture->format == SDL_PIXELFORMAT_YV12 || texture->format == SDL_PIXELFORMAT_P408 || texture->format == SDL_PIXELFORMAT_P416);
         BOOL nv12 = (texture->format == SDL_PIXELFORMAT_NV12 || texture->format == SDL_PIXELFORMAT_NV21 || texture->format == SDL_PIXELFORMAT_P010);
 
         if (yuv) {
-			mtltexdesc.pixelFormat = pixfmt;
+            mtltexdesc.pixelFormat = pixfmt;
             if (texture->format == SDL_PIXELFORMAT_P408 || texture->format == SDL_PIXELFORMAT_P416) {
                 mtltexdesc.width = texture->w;
                 mtltexdesc.height = texture->h;
@@ -810,8 +813,6 @@ static bool METAL_CreateTexture(SDL_Renderer *renderer, SDL_Texture *texture, SD
                 mtltexdesc.width = (texture->w + 1) / 2;
                 mtltexdesc.height = (texture->h + 1) / 2;
             }
-            mtltexdesc.textureType = MTLTextureType2DArray;
-            mtltexdesc.arrayLength = 2;
         } else if (texture->format == SDL_PIXELFORMAT_P010) {
             mtltexdesc.pixelFormat = MTLPixelFormatRG16Unorm;
             mtltexdesc.width = (texture->w + 1) / 2;
@@ -822,18 +823,40 @@ static bool METAL_CreateTexture(SDL_Renderer *renderer, SDL_Texture *texture, SD
             mtltexdesc.height = (texture->h + 1) / 2;
         }
 
-        if (yuv || nv12) {
-            if (surface) {
-                mtltextureUv = [data.mtldevice newTextureWithDescriptor:mtltexdesc iosurface:surface plane:1];
-            } else {
-                mtltextureUv = [data.mtldevice newTextureWithDescriptor:mtltexdesc];
+        if (yuv) {
+            mtltexture = (__bridge id<MTLTexture>)SDL_GetPointerProperty(create_props, SDL_PROP_TEXTURE_CREATE_METAL_TEXTURE_U_POINTER, nil);
+            if (mtltexture == nil) {
+                mtltexture = [data.mtldevice newTextureWithDescriptor:mtltexdesc];
             }
-            if (mtltextureUv == nil) {
+            if (mtltexture == nil) {
                 return SDL_SetError("Texture allocation failed");
             }
+            texturedata.mtltextureU = mtltexture;
+            SDL_SetPointerProperty(SDL_GetTextureProperties(texture), SDL_PROP_TEXTURE_METAL_TEXTURE_U_POINTER, (__bridge void *)mtltexture);
+
+            mtltexture = (__bridge id<MTLTexture>)SDL_GetPointerProperty(create_props, SDL_PROP_TEXTURE_CREATE_METAL_TEXTURE_V_POINTER, nil);
+            if (mtltexture == nil) {
+                mtltexture = [data.mtldevice newTextureWithDescriptor:mtltexdesc];
+            }
+            if (mtltexture == nil) {
+                return SDL_SetError("Texture allocation failed");
+            }
+            texturedata.mtltextureV = mtltexture;
+            SDL_SetPointerProperty(SDL_GetTextureProperties(texture), SDL_PROP_TEXTURE_METAL_TEXTURE_V_POINTER, (__bridge void *)mtltexture);
+
+        } else if (nv12) {
+            mtltexture = (__bridge id<MTLTexture>)SDL_GetPointerProperty(create_props, SDL_PROP_TEXTURE_CREATE_METAL_TEXTURE_UV_POINTER, nil);
+            if (mtltexture == nil) {
+                mtltexture = [data.mtldevice newTextureWithDescriptor:mtltexdesc];
+            }
+            if (mtltexture == nil) {
+                return SDL_SetError("Texture allocation failed");
+            }
+            texturedata.mtltextureUV = mtltexture;
+            SDL_SetPointerProperty(SDL_GetTextureProperties(texture), SDL_PROP_TEXTURE_METAL_TEXTURE_UV_POINTER, (__bridge void *)mtltexture);
         }
+
 #endif // SDL_HAVE_YUV
-        texturedata = [[SDL3METAL_TextureData alloc] init];
         if (texture->format == SDL_PIXELFORMAT_INDEX8) {
             texturedata.fragmentFunction = SDL_METAL_FRAGMENT_PALETTE;
 #ifdef SDL_HAVE_YUV
@@ -845,8 +868,6 @@ static bool METAL_CreateTexture(SDL_Renderer *renderer, SDL_Texture *texture, SD
         } else {
             texturedata.fragmentFunction = SDL_METAL_FRAGMENT_COPY;
         }
-        texturedata.mtltexture = mtltexture;
-        texturedata.mtltextureUv = mtltextureUv;
 #ifdef SDL_HAVE_YUV
         texturedata.yuv = yuv;
         texturedata.nv12 = nv12;
@@ -959,8 +980,9 @@ static bool METAL_UpdateTexture(SDL_Renderer *renderer, SDL_Texture *texture,
         }
 #ifdef SDL_HAVE_YUV
         if (texturedata.yuv) {
-            int Uslice = texture->format == SDL_PIXELFORMAT_YV12 ? 1 : 0;
-            int Vslice = texture->format == SDL_PIXELFORMAT_YV12 ? 0 : 1;
+            // YV12 stores V before U, so the plane order is swapped for it.
+            id<MTLTexture> firstplane = texture->format == SDL_PIXELFORMAT_YV12 ? texturedata.mtltextureV : texturedata.mtltextureU;
+            id<MTLTexture> secondplane = texture->format == SDL_PIXELFORMAT_YV12 ? texturedata.mtltextureU : texturedata.mtltextureV;
             int UVpitch;
             SDL_Rect UVrect;
             if (texture->format == SDL_PIXELFORMAT_P408 || texture->format == SDL_PIXELFORMAT_P416) {
@@ -976,13 +998,13 @@ static bool METAL_UpdateTexture(SDL_Renderer *renderer, SDL_Texture *texture,
 
             // Skip to the correct offset into the next texture
             pixels = (const void *)((const Uint8 *)pixels + rect->h * pitch);
-            if (!METAL_UpdateTextureInternal(renderer, texturedata.hasdata, texturedata.mtltextureUv, UVrect, Uslice, pixels, UVpitch)) {
+            if (!METAL_UpdateTextureInternal(renderer, texturedata.hasdata, firstplane, UVrect, 0, pixels, UVpitch)) {
                 return false;
             }
 
             // Skip to the correct offset into the next texture
             pixels = (const void *)((const Uint8 *)pixels + UVrect.h * UVpitch);
-            if (!METAL_UpdateTextureInternal(renderer, texturedata.hasdata, texturedata.mtltextureUv, UVrect, Vslice, pixels, UVpitch)) {
+            if (!METAL_UpdateTextureInternal(renderer, texturedata.hasdata, secondplane, UVrect, 0, pixels, UVpitch)) {
                 return false;
             }
         }
@@ -993,7 +1015,7 @@ static bool METAL_UpdateTexture(SDL_Renderer *renderer, SDL_Texture *texture,
 
             // Skip to the correct offset into the next texture
             pixels = (const void *)((const Uint8 *)pixels + rect->h * pitch);
-            if (!METAL_UpdateTextureInternal(renderer, texturedata.hasdata, texturedata.mtltextureUv, UVrect, 0, pixels, UVpitch)) {
+            if (!METAL_UpdateTextureInternal(renderer, texturedata.hasdata, texturedata.mtltextureUV, UVrect, 0, pixels, UVpitch)) {
                 return false;
             }
         }
@@ -1013,8 +1035,6 @@ static bool METAL_UpdateTextureYUV(SDL_Renderer *renderer, SDL_Texture *texture,
 {
     @autoreleasepool {
         SDL3METAL_TextureData *texturedata = (__bridge SDL3METAL_TextureData *)texture->internal;
-        const int Uslice = 0;
-        const int Vslice = 1;
         SDL_Rect UVrect;
         if (texture->format == SDL_PIXELFORMAT_P408 || texture->format == SDL_PIXELFORMAT_P416) {
             UVrect = *rect;
@@ -1033,10 +1053,10 @@ static bool METAL_UpdateTextureYUV(SDL_Renderer *renderer, SDL_Texture *texture,
         if (!METAL_UpdateTextureInternal(renderer, texturedata.hasdata, texturedata.mtltexture, *rect, 0, Yplane, Ypitch)) {
             return false;
         }
-        if (!METAL_UpdateTextureInternal(renderer, texturedata.hasdata, texturedata.mtltextureUv, UVrect, Uslice, Uplane, Upitch)) {
+        if (!METAL_UpdateTextureInternal(renderer, texturedata.hasdata, texturedata.mtltextureU, UVrect, 0, Uplane, Upitch)) {
             return false;
         }
-        if (!METAL_UpdateTextureInternal(renderer, texturedata.hasdata, texturedata.mtltextureUv, UVrect, Vslice, Vplane, Vpitch)) {
+        if (!METAL_UpdateTextureInternal(renderer, texturedata.hasdata, texturedata.mtltextureV, UVrect, 0, Vplane, Vpitch)) {
             return false;
         }
 
@@ -1064,7 +1084,7 @@ static bool METAL_UpdateTextureNV(SDL_Renderer *renderer, SDL_Texture *texture,
             return false;
         }
 
-        if (!METAL_UpdateTextureInternal(renderer, texturedata.hasdata, texturedata.mtltextureUv, UVrect, 0, UVplane, UVpitch)) {
+        if (!METAL_UpdateTextureInternal(renderer, texturedata.hasdata, texturedata.mtltextureUV, UVrect, 0, UVplane, UVpitch)) {
             return false;
         }
 
@@ -1149,8 +1169,9 @@ static void METAL_UnlockTexture(SDL_Renderer *renderer, SDL_Texture *texture)
               destinationOrigin:MTLOriginMake(rect.x, rect.y, 0)];
 #ifdef SDL_HAVE_YUV
         if (texturedata.yuv) {
-            int Uslice = texture->format == SDL_PIXELFORMAT_YV12 ? 1 : 0;
-            int Vslice = texture->format == SDL_PIXELFORMAT_YV12 ? 0 : 1;
+            // YV12 stores V before U, so the plane order is swapped for it.
+            id<MTLTexture> firstplane = texture->format == SDL_PIXELFORMAT_YV12 ? texturedata.mtltextureV : texturedata.mtltextureU;
+            id<MTLTexture> secondplane = texture->format == SDL_PIXELFORMAT_YV12 ? texturedata.mtltextureU : texturedata.mtltextureV;
             int UVpitch = (pitch + 1) / 2;
 
             [blitcmd copyFromBuffer:texturedata.lockedbuffer
@@ -1158,8 +1179,8 @@ static void METAL_UnlockTexture(SDL_Renderer *renderer, SDL_Texture *texture)
                   sourceBytesPerRow:UVpitch
                 sourceBytesPerImage:UVpitch * UVrect.h
                          sourceSize:MTLSizeMake(UVrect.w, UVrect.h, 1)
-                          toTexture:texturedata.mtltextureUv
-                   destinationSlice:Uslice
+                          toTexture:firstplane
+                   destinationSlice:0
                    destinationLevel:0
                   destinationOrigin:MTLOriginMake(UVrect.x, UVrect.y, 0)];
 
@@ -1168,8 +1189,8 @@ static void METAL_UnlockTexture(SDL_Renderer *renderer, SDL_Texture *texture)
                   sourceBytesPerRow:UVpitch
                 sourceBytesPerImage:UVpitch * UVrect.h
                          sourceSize:MTLSizeMake(UVrect.w, UVrect.h, 1)
-                          toTexture:texturedata.mtltextureUv
-                   destinationSlice:Vslice
+                          toTexture:secondplane
+                   destinationSlice:0
                    destinationLevel:0
                   destinationOrigin:MTLOriginMake(UVrect.x, UVrect.y, 0)];
         }
@@ -1182,7 +1203,7 @@ static void METAL_UnlockTexture(SDL_Renderer *renderer, SDL_Texture *texture)
                   sourceBytesPerRow:UVpitch
                 sourceBytesPerImage:0
                          sourceSize:MTLSizeMake(UVrect.w, UVrect.h, 1)
-                          toTexture:texturedata.mtltextureUv
+                          toTexture:texturedata.mtltextureUV
                    destinationSlice:0
                    destinationLevel:0
                   destinationOrigin:MTLOriginMake(UVrect.x, UVrect.y, 0)];
@@ -1708,7 +1729,12 @@ static bool SetCopyState(SDL_Renderer *renderer, const SDL_RenderCommand *cmd, c
         }
 #ifdef SDL_HAVE_YUV
         if (texturedata.yuv || texturedata.nv12) {
-            [data.mtlcmdencoder setFragmentTexture:texturedata.mtltextureUv atIndex:1];
+            if (texturedata.yuv) {
+                [data.mtlcmdencoder setFragmentTexture:texturedata.mtltextureU atIndex:1];
+                [data.mtlcmdencoder setFragmentTexture:texturedata.mtltextureV atIndex:2];
+            } else {
+                [data.mtlcmdencoder setFragmentTexture:texturedata.mtltextureUV atIndex:1];
+            }
             [data.mtlcmdencoder setFragmentBuffer:data.mtlbufconstants offset:texturedata.conversionBufferOffset atIndex:1];
         }
 #endif
@@ -1747,7 +1773,22 @@ static bool SetCopyState(SDL_Renderer *renderer, const SDL_RenderCommand *cmd, c
 
 static void METAL_InvalidateCachedState(SDL_Renderer *renderer)
 {
-    // METAL_DrawStateCache only exists during a run of METAL_RunCommandQueue, so there's nothing to invalidate!
+    @autoreleasepool {
+        SDL3METAL_RenderData *data = (__bridge SDL3METAL_RenderData *)renderer->internal;
+
+        // METAL_DrawStateCache only exists during a run of METAL_RunCommandQueue, so there's nothing to invalidate!
+
+        // Make sure any queued drawing is submitted before returning to the application
+        if (data.mtlcmdencoder != nil) {
+            [data.mtlcmdencoder endEncoding];
+            data.mtlcmdencoder = nil;
+        }
+
+        if (data.mtlcmdbuffer != nil) {
+            [data.mtlcmdbuffer commit];
+            data.mtlcmdbuffer = nil;
+        }
+    }
 }
 
 static bool METAL_RunCommandQueue(SDL_Renderer *renderer, SDL_RenderCommand *cmd, void *vertices, size_t vertsize)
@@ -2351,14 +2392,25 @@ static bool METAL_CreateRenderer(SDL_Renderer *renderer, SDL_Window *window, SDL
             }
         }
 
+        mtlcmdqueue = (__bridge id<MTLCommandQueue>)SDL_GetPointerProperty(create_props, SDL_PROP_RENDERER_CREATE_METAL_COMMAND_QUEUE_POINTER, nil);
+        if (mtlcmdqueue != nil) {
+            mtldevice = mtlcmdqueue.device;
+        }
+
+        if (mtldevice == nil) {
+            mtldevice = (__bridge id<MTLDevice>)SDL_GetPointerProperty(create_props, SDL_PROP_RENDERER_CREATE_METAL_DEVICE_POINTER, nil);
+        }
+
 #ifdef SDL_PLATFORM_MACOS
-        if (SDL_GetHintBoolean(SDL_HINT_RENDER_METAL_PREFER_LOW_POWER_DEVICE, true)) {
-            NSArray<id<MTLDevice>> *devices = MTLCopyAllDevices();
+        if (mtldevice == nil) {
+            if (SDL_GetHintBoolean(SDL_HINT_RENDER_METAL_PREFER_LOW_POWER_DEVICE, true)) {
+                NSArray<id<MTLDevice>> *devices = MTLCopyAllDevices();
 
-            for (id<MTLDevice> device in devices) {
-                if (device.isLowPower) {
-                    mtldevice = device;
-                    break;
+                for (id<MTLDevice> device in devices) {
+                    if (device.isLowPower) {
+                        mtldevice = device;
+                        break;
+                    }
                 }
             }
         }
@@ -2428,7 +2480,9 @@ in case we want to use it later (recreating the renderer)
 
         data.mtldevice = layer.device;
         data.mtllayer = layer;
-        mtlcmdqueue = [data.mtldevice newCommandQueue];
+        if (mtlcmdqueue == nil) {
+            mtlcmdqueue = [data.mtldevice newCommandQueue];
+        }
         data.mtlcmdqueue = mtlcmdqueue;
         data.mtlcmdqueue.label = @"SDL Metal Renderer";
         data.mtlpassdesc = [MTLRenderPassDescriptor renderPassDescriptor];
@@ -2556,27 +2610,30 @@ in case we want to use it later (recreating the renderer)
 #endif
 
         // https://developer.apple.com/metal/Metal-Feature-Set-Tables.pdf
-        maxtexsize = 4096;
 #if defined(SDL_PLATFORM_MACOS) || TARGET_OS_MACCATALYST
         maxtexsize = 16384;
 #elif defined(SDL_PLATFORM_TVOS)
-        maxtexsize = 8192;
         if ([mtldevice supportsFeatureSet:MTLFeatureSet_tvOS_GPUFamily2_v1]) {
             maxtexsize = 16384;
+        } else {
+            maxtexsize = 8192;
         }
 #else
-        if ([mtldevice supportsFeatureSet:MTLFeatureSet_iOS_GPUFamily4_v1]) {
-            maxtexsize = 16384;
-        } else if ([mtldevice supportsFeatureSet:MTLFeatureSet_iOS_GPUFamily3_v1]) {
+        if ([mtldevice supportsFeatureSet:MTLFeatureSet_iOS_GPUFamily4_v1] ||
+            [mtldevice supportsFeatureSet:MTLFeatureSet_iOS_GPUFamily3_v1]) {
             maxtexsize = 16384;
-        } else if ([mtldevice supportsFeatureSet:MTLFeatureSet_iOS_GPUFamily2_v2] || [mtldevice supportsFeatureSet:MTLFeatureSet_iOS_GPUFamily1_v2]) {
+        } else if ([mtldevice supportsFeatureSet:MTLFeatureSet_iOS_GPUFamily2_v2] ||
+                   [mtldevice supportsFeatureSet:MTLFeatureSet_iOS_GPUFamily1_v2]) {
             maxtexsize = 8192;
         } else {
             maxtexsize = 4096;
         }
 #endif
 
-        SDL_SetNumberProperty(SDL_GetRendererProperties(renderer), SDL_PROP_RENDERER_MAX_TEXTURE_SIZE_NUMBER, maxtexsize);
+        SDL_PropertiesID props = SDL_GetRendererProperties(renderer);
+        SDL_SetPointerProperty(props, SDL_PROP_RENDERER_METAL_DEVICE_POINTER, (__bridge void *)data.mtldevice);
+        SDL_SetPointerProperty(props, SDL_PROP_RENDERER_METAL_COMMAND_QUEUE_POINTER, (__bridge void *)data.mtlcmdqueue);
+        SDL_SetNumberProperty(props, SDL_PROP_RENDERER_MAX_TEXTURE_SIZE_NUMBER, maxtexsize);
 
         return true;
     }
diff --git a/src/render/metal/SDL_shaders_metal.metal b/src/render/metal/SDL_shaders_metal.metal
index d9bbf75e4ce09..7958130cb21ee 100644
--- a/src/render/metal/SDL_shaders_metal.metal
+++ b/src/render/metal/SDL_shaders_metal.metal
@@ -337,13 +337,14 @@ fragment float4 SDL_YUV_fragment(CopyVertexOutput vert [[stage_in]],
                                  constant ShaderConstants &c [[buffer(0)]],
                                  constant YUVDecode &decode [[buffer(1)]],
                                  texture2d<float> texY [[texture(0)]],
-                                 texture2d_array<float> texUV [[texture(1)]],
+                                 texture2d<float> texU [[texture(1)]],
+                                 texture2d<float> texV [[texture(2)]],
                                  sampler s [[sampler(0)]])
 {
     float3 yuv;
     yuv.x = texY.sample(s, vert.texcoord).r;
-    yuv.y = texUV.sample(s, vert.texcoord, 0).r;
-    yuv.z = texUV.sample(s, vert.texcoord, 1).r;
+    yuv.y = texU.sample(s, vert.texcoord).r;
+    yuv.z = texV.sample(s, vert.texcoord).r;
 
     float4 rgba;
     rgba.rgb = (yuv + decode.offset) * decode.matrix;
diff --git a/src/render/metal/SDL_shaders_metal_ios.h b/src/render/metal/SDL_shaders_metal_ios.h
index 303ffe33b7c6e..44557677a6ce4 100644
--- a/src/render/metal/SDL_shaders_metal_ios.h
+++ b/src/render/metal/SDL_shaders_metal_ios.h
@@ -1,28 +1,28 @@
 const unsigned char sdl_metallib[] = {
   0x4d, 0x54, 0x4c, 0x42, 0x01, 0x00, 0x02, 0x00, 0x02, 0x00, 0x00, 0x00,
-  0x00, 0x00, 0x00, 0x00, 0x21, 0xa4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0xb1, 0xa3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
   0x58, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4f, 0x03, 0x00, 0x00,
   0x00, 0x00, 0x00, 0x00, 0xab, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
   0x8e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x39, 0x04, 0x00, 0x00,
   0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-  0x71, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb0, 0x9f, 0x00, 0x00,
+  0x71, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x9f, 0x00, 0x00,
   0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00,
   0x4e, 0x41, 0x4d, 0x45, 0x11, 0x00, 0x53, 0x44, 0x4c, 0x5f, 0x53, 0x6f,
   0x6c, 0x69, 0x64, 0x5f, 0x76, 0x65, 0x72, 0x74, 0x65, 0x78, 0x00, 0x54,
   0x59, 0x50, 0x45, 0x01, 0x00, 0x00, 0x48, 0x41, 0x53, 0x48, 0x20, 0x00,
-  0x60, 0x7a, 0x81, 0x65, 0xda, 0xf6, 0x14, 0xc4, 0x56, 0x47, 0x0b, 0x7a,
-  0x77, 0x81, 0xf4, 0xc0, 0x01, 0xee, 0xcd, 0x26, 0x7c, 0x2d, 0x8d, 0x7b,
-  0x28, 0x66, 0x63, 0x5c, 0x85, 0xe9, 0xdd, 0xb9, 0x4f, 0x46, 0x46, 0x54,
+  0xe9, 0x4e, 0xb9, 0x7c, 0x66, 0x26, 0x51, 0xda, 0x95, 0x03, 0xbb, 0x2f,
+  0x16, 0x77, 0xdd, 0xdb, 0x2b, 0x8a, 0x66, 0xb8, 0x54, 0xe4, 0x9f, 0xf7,
+  0x7e, 0xa2, 0x3a, 0x3f, 0x67, 0x4a, 0x4f, 0xae, 0x4f, 0x46, 0x46, 0x54,
   0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
   0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
   0x00, 0x00, 0x56, 0x45, 0x52, 0x53, 0x08, 0x00, 0x01, 0x00, 0x08, 0x00,
   0x01, 0x00, 0x01, 0x00, 0x45, 0x4e, 0x44, 0x54, 0x77, 0x00, 0x00, 0x00,
   0x4e, 0x41, 0x4d, 0x45, 0x10, 0x00, 0x53, 0x44, 0x4c, 0x5f, 0x43, 0x6f,
   0x70, 0x79, 0x5f, 0x76, 0x65, 0x72, 0x74, 0x65, 0x78, 0x00, 0x54, 0x59,
-  0x50, 0x45, 0x01, 0x00, 0x00, 0x48, 0x41, 0x53, 0x48, 0x20, 0x00, 0x6e,
-  0xc3, 0x21, 0x35, 0x82, 0x00, 0x01, 0xe8, 0xa4, 0x27, 0xe1, 0xec, 0x73,
-  0xf1, 0xe9, 0x58, 0x2e, 0x4a, 0x0d, 0xc0, 0x5a, 0x63, 0x1e, 0x7f, 0x35,
-  0xf1, 0x6e, 0x35, 0x4e, 0xbf, 0x18, 0xae, 0x4f, 0x46, 0x46, 0x54, 0x18,
+  0x50, 0x45, 0x01, 0x00, 0x00, 0x48, 0x41, 0x53, 0x48, 0x20, 0x00, 0xed,
+  0x0f, 0xc1, 0xa4, 0x27, 0xa9, 0x18, 0x07, 0x3d, 0xfb, 0x0c, 0xb6, 0x92,
+  0xaf, 0x0c, 0xb2, 0x66, 0xf7, 0x24, 0x24, 0x41, 0xf5, 0xd9, 0x1d, 0xac,
+  0x1f, 0xdc, 0xc0, 0x8d, 0xfa, 0xb6, 0x03, 0x4f, 0x46, 0x46, 0x54, 0x18,
   0x00, 0x2d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00,
   0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00,
   0x00, 0x56, 0x45, 0x52, 0x53, 0x08, 0x00, 0x01, 0x00, 0x08, 0x00, 0x01,
@@ -30,9 +30,9 @@ const unsigned char sdl_metallib[] = 

(Patch may be truncated, please check the link at the top of this post.)