SDL: Perform Vulkan YUV color conversion explicitly in shaders

From 26b37f5d1c3518125ff8b269b5bdfcf452870500 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Sat, 12 Sep 2026 11:49:44 -0700
Subject: [PATCH] Perform Vulkan YUV color conversion explicitly in shaders

This makes the Vulkan code match the D3D/Metal/GPU renderers, and avoids bugs and unsupported format/sampler combinations on various vendor's drivers (including NVIDIA!)
---
 include/SDL3/SDL_render.h                     |  25 +-
 src/render/SDL_render.c                       |   4 +
 src/render/vulkan/SDL_render_vulkan.c         | 613 +++++++-----
 src/render/vulkan/SDL_shaders_vulkan.c        |   6 -
 src/render/vulkan/SDL_shaders_vulkan.h        |   2 -
 .../vulkan/VULKAN_PixelShader_Advanced.h      | 927 ++++++++++--------
 .../vulkan/VULKAN_PixelShader_Advanced_YUV.h  | 415 --------
 src/render/vulkan/VULKAN_PixelShader_Colors.h |  53 +-
 .../vulkan/VULKAN_PixelShader_Colors_PQ.h     |  77 +-
 .../vulkan/VULKAN_PixelShader_Common.hlsli    |  48 +-
 .../vulkan/VULKAN_PixelShader_Textures.h      |  69 +-
 .../vulkan/VULKAN_PixelShader_Textures_PQ.h   | 765 ++++++++-------
 .../VULKAN_PixelShader_Textures_Simple.h      |   6 +-
 .../vulkan/VULKAN_PixelShader_Textures_YUV.h  |  52 -
 src/render/vulkan/compile_shaders.bat         |   2 -
 15 files changed, 1471 insertions(+), 1593 deletions(-)
 delete mode 100644 src/render/vulkan/VULKAN_PixelShader_Advanced_YUV.h
 delete mode 100644 src/render/vulkan/VULKAN_PixelShader_Textures_YUV.h

diff --git a/include/SDL3/SDL_render.h b/include/SDL3/SDL_render.h
index 8baaf143d32f4..579cea3dac0af 100644
--- a/include/SDL3/SDL_render.h
+++ b/include/SDL3/SDL_render.h
@@ -803,9 +803,19 @@ extern SDL_DECLSPEC SDL_Texture * SDLCALL SDL_CreateTextureFromSurface(SDL_Rende
  * With the vulkan renderer:
  *
  * - `SDL_PROP_TEXTURE_CREATE_VULKAN_TEXTURE_NUMBER`: the VkImage associated
- *   with the texture, if you want to wrap an existing texture.
+ *   with the texture, if you want to wrap an existing texture. For NV12 style
+ *   textures this is the single two plane VkImage holding both the Y and UV
+ *   planes, and for YUV style textures it is the VkImage holding the Y plane.
+ * - `SDL_PROP_TEXTURE_CREATE_VULKAN_TEXTURE_U_NUMBER`: the VkImage associated
+ *   with the U plane of a YUV texture, if you want to wrap an existing
+ *   texture.
+ * - `SDL_PROP_TEXTURE_CREATE_VULKAN_TEXTURE_V_NUMBER`: the VkImage associated
+ *   with the V plane of a YUV texture, if you want to wrap an existing
+ *   texture.
  * - `SDL_PROP_TEXTURE_CREATE_VULKAN_LAYOUT_NUMBER`: the VkImageLayout for the
  *   VkImage, defaults to VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL.
+ * - `SDL_PROP_TEXTURE_CREATE_VULKAN_USAGE_NUMBER`: additional VK_IMAGE_USAGE bits that should be used when creating the texture.
+ *   VkImage, defaults to VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL.
  * - `SDL_PROP_TEXTURE_CREATE_VULKAN_ANDROID_HARDWARE_BUFFER_POINTER`: the AHardwareBuffer to sample from, if you want to use an existing Android hardware buffer as the texture. You must use SDL_PIXELFORMAT_EXTERNAL_OES for the texture format.
  *   You can't directly update the texture or use it as a render target. If the Android buffer contents change, you must recreate the texture to pick up the changes.
  *   The texture holds a reference to the buffer, so you can release your own reference once the texture has been created.
@@ -871,7 +881,10 @@ extern SDL_DECLSPEC SDL_Texture * SDLCALL SDL_CreateTextureWithProperties(SDL_Re
 #define SDL_PROP_TEXTURE_CREATE_OPENGLES2_TEXTURE_U_NUMBER      "SDL.texture.create.opengles2.texture_u"
 #define SDL_PROP_TEXTURE_CREATE_OPENGLES2_TEXTURE_V_NUMBER      "SDL.texture.create.opengles2.texture_v"
 #define SDL_PROP_TEXTURE_CREATE_VULKAN_TEXTURE_NUMBER           "SDL.texture.create.vulkan.texture"
+#define SDL_PROP_TEXTURE_CREATE_VULKAN_TEXTURE_U_NUMBER         "SDL.texture.create.vulkan.texture_u"
+#define SDL_PROP_TEXTURE_CREATE_VULKAN_TEXTURE_V_NUMBER         "SDL.texture.create.vulkan.texture_v"
 #define SDL_PROP_TEXTURE_CREATE_VULKAN_LAYOUT_NUMBER            "SDL.texture.create.vulkan.layout"
+#define SDL_PROP_TEXTURE_CREATE_VULKAN_USAGE_NUMBER             "SDL.texture.create.vulkan.usage"
 #define SDL_PROP_TEXTURE_CREATE_VULKAN_ANDROID_HARDWARE_BUFFER_POINTER "SDL.texture.create.vulkan.android_hardware_buffer"
 #define SDL_PROP_TEXTURE_CREATE_GPU_TEXTURE_POINTER             "SDL.texture.create.gpu.texture"
 #define SDL_PROP_TEXTURE_CREATE_GPU_TEXTURE_UV_POINTER          "SDL.texture.create.gpu.texture_uv"
@@ -935,7 +948,13 @@ extern SDL_DECLSPEC SDL_Texture * SDLCALL SDL_CreateTextureWithProperties(SDL_Re
  * With the vulkan renderer:
  *
  * - `SDL_PROP_TEXTURE_VULKAN_TEXTURE_NUMBER`: the VkImage associated with the
- *   texture
+ *   texture. For NV12 style textures this is the single two plane VkImage
+ *   holding both the Y and UV planes, and for YUV style textures it is the
+ *   VkImage holding the Y plane.
+ * - `SDL_PROP_TEXTURE_VULKAN_TEXTURE_U_NUMBER`: the VkImage associated with
+ *   the U plane of a YUV texture
+ * - `SDL_PROP_TEXTURE_VULKAN_TEXTURE_V_NUMBER`: the VkImage associated with
+ *   the V plane of a YUV texture
  *
  * With the opengl renderer:
  *
@@ -1018,6 +1037,8 @@ extern SDL_DECLSPEC SDL_PropertiesID SDLCALL SDL_GetTextureProperties(SDL_Textur
 #define SDL_PROP_TEXTURE_OPENGLES2_TEXTURE_V_NUMBER         "SDL.texture.opengles2.texture_v"
 #define SDL_PROP_TEXTURE_OPENGLES2_TEXTURE_TARGET_NUMBER    "SDL.texture.opengles2.target"
 #define SDL_PROP_TEXTURE_VULKAN_TEXTURE_NUMBER              "SDL.texture.vulkan.texture"
+#define SDL_PROP_TEXTURE_VULKAN_TEXTURE_U_NUMBER            "SDL.texture.vulkan.texture_u"
+#define SDL_PROP_TEXTURE_VULKAN_TEXTURE_V_NUMBER            "SDL.texture.vulkan.texture_v"
 #define SDL_PROP_TEXTURE_GPU_TEXTURE_POINTER                "SDL.texture.gpu.texture"
 #define SDL_PROP_TEXTURE_GPU_TEXTURE_UV_POINTER             "SDL.texture.gpu.texture_uv"
 #define SDL_PROP_TEXTURE_GPU_TEXTURE_U_POINTER              "SDL.texture.gpu.texture_u"
diff --git a/src/render/SDL_render.c b/src/render/SDL_render.c
index 54dadec545d18..059eaf5f6d804 100644
--- a/src/render/SDL_render.c
+++ b/src/render/SDL_render.c
@@ -1523,6 +1523,10 @@ SDL_Texture *SDL_CreateTextureWithProperties(SDL_Renderer *renderer, SDL_Propert
         SDL_SetError("Palettized textures can't be render targets");
         return NULL;
     }
+    CHECK_PARAM(SDL_ISPIXELFORMAT_FOURCC(format) && access == SDL_TEXTUREACCESS_TARGET) {
+        SDL_SetError("%s textures can't be render targets", SDL_GetPixelFormatName(format));
+        return NULL;
+    }
     CHECK_PARAM(w <= 0 || h <= 0) {
         SDL_SetError("Texture dimensions can't be 0");
         return NULL;
diff --git a/src/render/vulkan/SDL_render_vulkan.c b/src/render/vulkan/SDL_render_vulkan.c
index 392a31d45c23a..39abd1ca49da9 100644
--- a/src/render/vulkan/SDL_render_vulkan.c
+++ b/src/render/vulkan/SDL_render_vulkan.c
@@ -28,6 +28,7 @@
 #define SDL_VULKAN_CONSTANT_BUFFER_DEFAULT_SIZE 65536
 #define SDL_VULKAN_NUM_UPLOAD_BUFFERS           32
 #define SDL_VULKAN_MAX_DESCRIPTOR_SETS          4096
+#define SDL_VULKAN_NUM_TEXTURE_BINDINGS         3
 
 #define SDL_VULKAN_VALIDATION_LAYER_NAME        "VK_LAYER_KHRONOS_validation"
 
@@ -202,6 +203,9 @@ static const float TEXTURETYPE_RGB_PIXELART = 2;
 static const float TEXTURETYPE_PALETTE_NEAREST = 3;
 static const float TEXTURETYPE_PALETTE_LINEAR = 4;
 static const float TEXTURETYPE_PALETTE_PIXELART = 5;
+static const float TEXTURETYPE_NV12 = 6;
+static const float TEXTURETYPE_NV21 = 7;
+static const float TEXTURETYPE_YUV = 8;
 
 static const float INPUTTYPE_UNSPECIFIED = 0;
 static const float INPUTTYPE_SRGB = 1;
@@ -225,6 +229,8 @@ typedef struct
     float tonemap_factor1;
     float tonemap_factor2;
     float sdr_white_point;
+
+    float YCbCr_matrix[16];
 } VULKAN_PixelShaderConstants;
 
 // Per-vertex data
@@ -263,7 +269,6 @@ typedef struct
 {
     bool allocatedImage;
     VkImage image;
-    VkImageView imageView;
     VkDeviceMemory deviceMemory;
     VkImageLayout imageLayout;
     VkFormat format;
@@ -273,6 +278,7 @@ typedef struct
 typedef struct
 {
     VULKAN_Image image;
+    VkImageView imageView;
 } VULKAN_PaletteData;
 
 // YUV conversion data
@@ -307,19 +313,23 @@ typedef struct VULKAN_YUVPipeline
 // Per-texture data
 typedef struct
 {
-    VULKAN_Image mainImage;
+    int numImages;
+    VULKAN_Image images[SDL_VULKAN_NUM_TEXTURE_BINDINGS];
+    int numImageViews;
+    VkImageView imageViews[SDL_VULKAN_NUM_TEXTURE_BINDINGS];
     VkRenderPass mainRenderpasses[VULKAN_RENDERPASS_COUNT];
     VkFramebuffer mainFramebuffer;
     VULKAN_Buffer stagingBuffer;
-    SDL_Rect lockedRect;
     int width;
     int height;
+    const float *YCbCr_matrix;
 
-    bool yuv;
+    // YCbCr conversion support
     VULKAN_YUVPipeline *yuvPipeline;
 
     Uint8 *pixels;
     int pitch;
+    SDL_Rect lockedRect;
 } VULKAN_TextureData;
 
 // Pipeline State Object data
@@ -485,6 +495,38 @@ static SDL_PixelFormat VULKAN_VkFormatToSDLPixelFormat(VkFormat vkFormat)
     return SDL_PIXELFORMAT_UNKNOWN;
 }
 
+static int VULKAN_GetFormatImageCount(SDL_PixelFormat format)
+{
+    switch (format) {
+    case SDL_PIXELFORMAT_YV12:
+    case SDL_PIXELFORMAT_IYUV:
+    case SDL_PIXELFORMAT_I444:
+    case SDL_PIXELFORMAT_I0FL:
+    case SDL_PIXELFORMAT_I4FL:
+        return 3;
+    default:
+        return 1;
+    }
+}
+
+static int VULKAN_GetFormatImageViewCount(SDL_PixelFormat format)
+{
+    switch (format) {
+    case SDL_PIXELFORMAT_NV12:
+    case SDL_PIXELFORMAT_NV21:
+    case SDL_PIXELFORMAT_P010:
+        return 2;
+    case SDL_PIXELFORMAT_YV12:
+    case SDL_PIXELFORMAT_IYUV:
+    case SDL_PIXELFORMAT_I444:
+    case SDL_PIXELFORMAT_I0FL:
+    case SDL_PIXELFORMAT_I4FL:
+        return 3;
+    default:
+        return 1;
+    }
+}
+
 static int VULKAN_VkFormatGetNumPlanes(VkFormat vkFormat)
 {
     switch (vkFormat) {
@@ -536,18 +578,16 @@ static VkFormat SDLPixelFormatToVkTextureFormat(SDL_PixelFormat format, Uint32 o
         return VK_FORMAT_B8G8R8G8_422_UNORM;
     case SDL_PIXELFORMAT_YV12:
     case SDL_PIXELFORMAT_IYUV:
-        return VK_FORMAT_G8_B8_R8_3PLANE_420_UNORM;
     case SDL_PIXELFORMAT_I444:
-        return VK_FORMAT_G8_B8_R8_3PLANE_444_UNORM;
+        return VK_FORMAT_R8_UNORM;
     case SDL_PIXELFORMAT_NV12:
     case SDL_PIXELFORMAT_NV21:
         return  VK_FORMAT_G8_B8R8_2PLANE_420_UNORM;
     case SDL_PIXELFORMAT_P010:
         return VK_FORMAT_G10X6_B10X6R10X6_2PLANE_420_UNORM_3PACK16;
     case SDL_PIXELFORMAT_I0FL:
-        return VK_FORMAT_G16_B16_R16_3PLANE_420_UNORM;
     case SDL_PIXELFORMAT_I4FL:
-        return VK_FORMAT_G16_B16_R16_3PLANE_444_UNORM;
+        return VK_FORMAT_R16_UNORM;
     default:
         for (int i = 0; i < SDL_arraysize(vk_format_map); i++) {
             if (vk_format_map[i].sdl == format) {
@@ -563,6 +603,63 @@ static VkFormat SDLPixelFormatToVkTextureFormat(SDL_PixelFormat format, Uint32 o
     }
 }
 
+static VkFormat VULKAN_GetVkImageFormat(SDL_PixelFormat format, Uint32 output_colorspace)
+{
+    switch (format) {
+    case SDL_PIXELFORMAT_INDEX8:
+        return VK_FORMAT_R8_UNORM;
+    case SDL_PIXELFORMAT_YUY2:
+        return VK_FORMAT_G8B8G8R8_422_UNORM;
+    case SDL_PIXELFORMAT_UYVY:
+        return VK_FORMAT_B8G8R8G8_422_UNORM;
+    case SDL_PIXELFORMAT_YV12:
+    case SDL_PIXELFORMAT_IYUV:
+    case SDL_PIXELFORMAT_I444:
+        return VK_FORMAT_R8_UNORM;
+    case SDL_PIXELFORMAT_NV12:
+    case SDL_PIXELFORMAT_NV21:
+        return  VK_FORMAT_G8_B8R8_2PLANE_420_UNORM;
+    case SDL_PIXELFORMAT_P010:
+        return VK_FORMAT_G10X6_B10X6R10X6_2PLANE_420_UNORM_3PACK16;
+    case SDL_PIXELFORMAT_I0FL:
+    case SDL_PIXELFORMAT_I4FL:
+        return VK_FORMAT_R16_UNORM;
+    default:
+        for (int i = 0; i < SDL_arraysize(vk_format_map); i++) {
+            if (vk_format_map[i].sdl == format) {
+                if (output_colorspace == SDL_COLORSPACE_SRGB_LINEAR ||
+                    output_colorspace == SDL_COLORSPACE_HDR10) {
+                    return vk_format_map[i].srgb;
+                } else {
+                    return vk_format_map[i].unorm;
+                }
+            }
+        }
+        return VK_FORMAT_UNDEFINED;
+    }
+}
+
+static VkFormat VULKAN_GetVkImageViewFormat(SDL_PixelFormat format, int plane, Uint32 output_colorspace)
+{
+    switch (format) {
+    case SDL_PIXELFORMAT_NV12:
+    case SDL_PIXELFORMAT_NV21:
+        if (plane == 0) {
+            return VK_FORMAT_R8_UNORM;
+        } else {
+            return VK_FORMAT_R8G8_UNORM;
+        }
+    case SDL_PIXELFORMAT_P010:
+        if (plane == 0) {
+            return VK_FORMAT_R8_UNORM;
+        } else {
+            return VK_FORMAT_R16G16_UNORM;
+        }
+    default:
+        return VULKAN_GetVkImageFormat(format, output_colorspace);
+    }
+}
+
 static void VULKAN_DestroyTexture(SDL_Renderer *renderer, SDL_Texture *texture);
 static void VULKAN_DestroyBuffer(VULKAN_RenderData *rendererData, VULKAN_Buffer *vulkanBuffer);
 static void VULKAN_DestroyImage(VULKAN_RenderData *rendererData, VULKAN_Image *vulkanImage);
@@ -574,7 +671,9 @@ static bool VULKAN_FindMemoryTypeIndex(VULKAN_RenderData *rendererData, uint32_t
 static VkResult VULKAN_CreateWindowSizeDependentResources(SDL_Renderer *renderer);
 static VkDescriptorPool VULKAN_AllocateDescriptorPool(VULKAN_RenderData *rendererData);
 static VkResult VULKAN_CreateDescriptorSetAndPipelineLayout(VULKAN_RenderData *rendererData, VkSampler samplerYcbcr, VkDescriptorSetLayout *descriptorSetLayoutOut, VkPipelineLayout *pipelineLayoutOut);
+#ifdef SDL_PLATFORM_ANDROID
 static VULKAN_YUVPipeline *VULKAN_GetYUVPipeline(VULKAN_RenderData *rendererData, const VULKAN_YUVPipelineCreateInfo *createInfo);
+#endif
 static void VULKAN_CleanupYUVPipeline(VULKAN_RenderData *rendererData, VULKAN_YUVPipeline *pipeline);
 static VkSurfaceTransformFlagBitsKHR VULKAN_GetRotationForCurrentRenderTarget(VULKAN_RenderData *rendererData);
 static bool VULKAN_IsDisplayRotated90Degrees(VkSurfaceTransformFlagBitsKHR rotation);
@@ -854,10 +953,6 @@ static VkResult VULKAN_AllocateBuffer(VULKAN_RenderData *rendererData, VkDeviceS
 
 static void VULKAN_DestroyImage(VULKAN_RenderData *rendererData, VULKAN_Image *vulkanImage)
 {
-    if (vulkanImage->imageView != VK_NULL_HANDLE) {
-        vkDestroyImageView(rendererData->device, vulkanImage->imageView, NULL);
-        vulkanImage->imageView = VK_NULL_HANDLE;
-    }
     if (vulkanImage->image != VK_NULL_HANDLE) {
         if (vulkanImage->allocatedImage) {
             vkDestroyImage(rendererData->device, vulkanImage->image, NULL);
@@ -871,17 +966,15 @@ static void VULKAN_DestroyImage(VULKAN_RenderData *rendererData, VULKAN_Image *v
         }
         vulkanImage->deviceMemory = VK_NULL_HANDLE;
     }
-    SDL_zerop(vulkanImage);
 }
 
-static VkResult VULKAN_AllocateImage(VULKAN_RenderData *rendererData, SDL_PropertiesID create_props, uint32_t width, uint32_t height, VkFormat format, VkImageUsageFlags imageUsage, VkComponentMapping swizzle, VkSamplerYcbcrConversionKHR samplerYcbcrConversion, VULKAN_ExternalBufferInfo *external, VULKAN_Image *imageOut)
+static VkResult VULKAN_AllocateImage(VULKAN_RenderData *rendererData, SDL_PropertiesID create_props, const char *property, uint32_t width, uint32_t height, VkFormat format, VkImageUsageFlags imageUsage, VkSamplerYcbcrConversionKHR samplerYcbcrConversion, VULKAN_ExternalBufferInfo *external, VULKAN_Image *imageOut)
 {
     VkResult result;
-    VkSamplerYcbcrConversionInfoKHR samplerYcbcrConversionInfo = { 0 };
 
     SDL_zerop(imageOut);
     imageOut->format = format;
-    imageOut->image = (VkImage)SDL_GetNumberProperty(create_props, SDL_PROP_TEXTURE_CREATE_VULKAN_TEXTURE_NUMBER, 0);
+    imageOut->image = (VkImage)SDL_GetNumberProperty(create_props, property, 0);
 
     if (imageOut->image == VK_NULL_HANDLE) {
         imageOut->allocatedImage = VK_TRUE;
@@ -903,6 +996,11 @@ static VkResult VULKAN_AllocateImage(VULKAN_RenderData *rendererData, SDL_Proper
         imageCreateInfo.queueFamilyIndexCount = 0;
         imageCreateInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
 
+        if (VULKAN_VkFormatGetNumPlanes(format) > 0) {
+            // We'll take image views with a different format
+            imageCreateInfo.flags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
+        }
+
 #ifdef SDL_PLATFORM_ANDROID
         VkExternalMemoryImageCreateInfoKHR externalMemoryImageCreateInfo = { 0 };
         if (external) {
@@ -913,12 +1011,6 @@ static VkResult VULKAN_AllocateImage(VULKAN_RenderData *rendererData, SDL_Proper
         }
 #endif
 
-        // Allow writing to planar YUV textures from application code
-        if (VULKAN_VkFormatGetNumPlanes(format) > 1 && !external) {
-            imageCreateInfo.flags |= (VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | VK_IMAGE_CREATE_EXTENDED_USAGE_BIT);
-            imageCreateInfo.usage |= VK_IMAGE_USAGE_STORAGE_BIT;
-        }
-
         result = vkCreateImage(rendererData->device, &imageCreateInfo, NULL, &imageOut->image);
         if (result != VK_SUCCESS) {
             VULKAN_DestroyImage(rendererData, imageOut);
@@ -1006,13 +1098,28 @@ static VkResult VULKAN_AllocateImage(VULKAN_RenderData *rendererData, SDL_Proper
         }
     }
 
+    return VK_SUCCESS;
+}
+
+static VkResult VULKAN_AllocateImageView(VULKAN_RenderData *rendererData, VkImage image, int plane, VkFormat format, VkImageUsageFlags imageUsage, VkComponentMapping swizzle, VkSamplerYcbcrConversionKHR samplerYcbcrConversion, VkImageView *imageViewOut)
+{
+    VkResult result;
+    VkSamplerYcbcrConversionInfoKHR samplerYcbcrConversionInfo = { 0 };
+    VkImageAspectFlags aspectMask;
+
+    if (plane < 0) {
+        aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
+    } else {
+        aspectMask = (VkImageAspectFlags)(VK_IMAGE_ASPECT_PLANE_0_BIT << plane);
+    }
+
     VkImageViewCreateInfo imageViewCreateInfo = { 0 };
     imageViewCreateInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
-    imageViewCreateInfo.image = imageOut->image;
+    imageViewCreateInfo.image = image;
     imageViewCreateInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
     imageViewCreateInfo.format = format;
     imageViewCreateInfo.components = swizzle;
-    imageViewCreateInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
+    imageViewCreateInfo.subresourceRange.aspectMask = aspectMask;
     imageViewCreateInfo.subresourceRange.baseMipLevel = 0;
     imageViewCreateInfo.subresourceRange.levelCount = 1;
     imageViewCreateInfo.subresourceRange.baseArrayLayer = 0;
@@ -1023,24 +1130,24 @@ static VkResult VULKAN_AllocateImage(VULKAN_RenderData *rendererData, SDL_Proper
     imageViewUsageCreateInfo.usage = imageUsage;
     imageViewCreateInfo.pNext = &imageViewUsageCreateInfo;
 
-    // If it's a YCbCr image, we need to pass the conversion info to the VkImageView (and the VkSampler)
+    // If it's a YCbCr image, we need to pass the conversion info to the VkImageView
     if (samplerYcbcrConversion != VK_NULL_HANDLE) {
         samplerYcbcrConversionInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_YCBCR_CONVERSION_INFO_KHR;
         samplerYcbcrConversionInfo.conversion = samplerYcbcrConversion;
         imageViewUsageCreateInfo.pNext = &samplerYcbcrConversionInfo;
     }
 
-    result = vkCreateImageView(rendererData->device, &imageViewCreateInfo, NULL, &imageOut->imageView);
+    result = vkCreateImageView(rendererData->device, &imageViewCreateInfo, NULL, imageViewOut);
     if (result != VK_SUCCESS) {
-        VULKAN_DestroyImage(rendererData, imageOut);
         SET_ERROR_CODE("vkCreateImageView()", result);
         return result;
     }
 
-    return result;
+    return VK_SUCCESS;
 }
 
 
+
 static void VULKAN_RecordExternalImageBarrier(VULKAN_RenderData *rendererData, VkAccessFlags sourceAccessMask, VkAccessFlags destAccessMask,
     VkPipelineStageFlags srcStageFlags, VkPipelineStageFlags dstStageFlags, VkImageLayout destLayout, VkImage image, VkImageLayout *imageLayout)
 {
@@ -2863,7 +2970,12 @@ static bool VULKAN_CreatePalette(SDL_Renderer *renderer, SDL_TexturePalette *pal
     VkFormat format = SDLPixelFormatToVkTextureFormat(SDL_PIXELFORMAT_RGBA32, renderer->output_colorspace);
     VkImageUsageFlags usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
     VkComponentMapping imageViewSwizzle = data->identitySwizzle;
-    VkResult result = VULKAN_AllocateImage(data, 0, 256, 1, format, usage, imageViewSwizzle, VK_NULL_HANDLE, NULL, &palettedata->image);
+    VkResult result = VULKAN_AllocateImage(data, 0, NULL, 256, 1, format, usage, VK_NULL_HANDLE, NULL, &palettedata->image);
+    if (result != VK_SUCCESS) {
+        SET_ERROR_CODE("VULKAN_AllocateImage()", result);
+        return false;
+    }
+    result = VULKAN_AllocateImageView(data, palettedata->image.image, -1, format, usage, imageViewSwizzle, VK_NULL_HANDLE, &palettedata->imageView);
     if (result != VK_SUCCESS) {
         SET_ERROR_CODE("VULKAN_AllocateImage()", result);
         return false;
@@ -2894,6 +3006,11 @@ static void VULKAN_DestroyPalette(SDL_Renderer *renderer, SDL_TexturePalette *pa
     VULKAN_WaitForGPU(data);
 
     VULKAN_DestroyImage(data, &palettedata->image);
+
+    if (palettedata->imageView != VK_NULL_HANDLE) {
+        vkDestroyImageView(data->device, palettedata->imageView, NULL);
+        palettedata->imageView = VK_NULL_HANDLE;
+    }
     SDL_free(palettedata);
 }
 
@@ -2926,9 +3043,13 @@ static bool VULKAN_CreateTexture(SDL_Renderer *renderer, SDL_Texture *texture, S
     VULKAN_RenderData *rendererData = (VULKAN_RenderData *)renderer->internal;
     VULKAN_TextureData *textureData;
     VkResult result;
-    VkFormat textureFormat = SDLPixelFormatToVkTextureFormat(texture->format, renderer->output_colorspace);
+    int numImages = VULKAN_GetFormatImageCount(texture->format);
+    int numImageViews = VULKAN_GetFormatImageViewCount(texture->format);
+    VkFormat textureFormat = VULKAN_GetVkImageFormat(texture->format, renderer->output_colorspace);
     uint32_t width = texture->w;
     uint32_t height = texture->h;
+    uint32_t chromaWidth = width;
+    uint32_t chromaHeight = height;
     VkComponentMapping imageViewSwizzle = rendererData->identitySwizzle;
     VULKAN_ExternalBufferInfo externalBufferInfo;
     VULKAN_ExternalBufferInfo *external = NULL;
@@ -2937,10 +3058,6 @@ static bool VULKAN_CreateTexture(SDL_Renderer *renderer, SDL_Texture *texture, S
         return SDL_SetError("Device lost and couldn't be recovered");
     }
 
-    if (textureFormat == VK_FORMAT_UNDEFINED && texture->format != SDL_PIXELFORMAT_EXTERNAL_OES) {
-        return SDL_SetError("%s, An unsupported SDL pixel format (0x%x) was specified", SDL_FUNCTION, texture->format);
-    }
-
     textureData = (VULKAN_TextureData *)SDL_calloc(1, sizeof(*textureData));
     if (!textureData) {
         return false;
@@ -2951,10 +3068,6 @@ static bool VULKAN_CreateTexture(SDL_Renderer *renderer, SDL_Texture *texture, S
         SDL_zero(externalBufferInfo);
         external = &externalBufferInfo;
 
-        if (texture->access == SDL_TEXTUREACCESS_TARGET) {
-            return SDL_SetError("SDL_PIXELFORMAT_EXTERNAL_OES textures can't be render targets");
-        }
-
 #ifdef SDL_PLATFORM_ANDROID
         external->buffer = (struct AHardwareBuffer *)SDL_GetPointerProperty(create_props, SDL_PROP_TEXTURE_CREATE_VULKAN_ANDROID_HARDWARE_BUFFER_POINTER, NULL);
         if (!external->buffer) {
@@ -3061,132 +3174,97 @@ static bool VULKAN_CreateTexture(SDL_Renderer *renderer, SDL_Texture *texture, S
             if (!textureData->yuvPipeline) {
                 return false;
             }
-
-            textureData->yuv = true;
         }
 #endif // SDL_PLATFORM_ANDROID
-    } else if (texture->format == SDL_PIXELFORMAT_YV12 ||
-               texture->format == SDL_PIXELFORMAT_IYUV ||
-               texture->format == SDL_PIXELFORMAT_I444 ||
-               texture->format == SDL_PIXELFORMAT_NV12 ||
-               texture->format == SDL_PIXELFORMAT_NV21 ||
-               texture->format == SDL_PIXELFORMAT_P010 ||
-               texture->format == SDL_PIXELFORMAT_I0FL ||
-               texture->format == SDL_PIXELFORMAT_I4FL) {
-        const uint32_t YUV_SD_THRESHOLD = 576;
-
-        VULKAN_YUVPipelineCreateInfo createInfo;
-        SDL_zero(createInfo); // Must zero padding bytes for cache
-        createInfo.filter = VK_FILTER_LINEAR;
-
-        VkSamplerYcbcrConversionCreateInfoKHR *samplerYcbcrConversionCreateInfo = &createInfo.samplerYcbcrConversionCreateInfo;
-        samplerYcbcrConversionCreateInfo->sType = VK_STRUCTURE_TYPE_SAMPLER_YCBCR_CONVERSION_CREATE_INFO_KHR;
-
-        if (texture->format != SDL_PIXELFORMAT_I444 &&
-            texture->format != SDL_PIXELFORMAT_I4FL) {
+    } else if (SDL_ISPIXELFORMAT_FOURCC(texture->format)) {
+        if (texture->format == SDL_PIXELFORMAT_YV12 ||
+            texture->format == SDL_PIXELFORMAT_IYUV ||
+            texture->format == SDL_PIXELFORMAT_NV12 ||
+            texture->format == SDL_PIXELFORMAT_NV21 ||
+            texture->format == SDL_PIXELFORMAT_P010 ||
+            texture->format == SDL_PIXELFORMAT_I0FL) {
             // Pad width/height to multiple of 2
             width = (width + 1) & ~1;
             height = (height + 1) & ~1;
         }
-
-        // Create samplerYcbcrConversion which will be used on the VkImageView and VkSampler
-        samplerYcbcrConversionCreateInfo->format = textureFormat;
-        switch (SDL_COLORSPACEMATRIX(texture->colorspace)) {
-        case SDL_MATRIX_COEFFICIENTS_BT470BG:
-        case SDL_MATRIX_COEFFICIENTS_BT601:
-            samplerYcbcrConversionCreateInfo->ycbcrModel = VK_SAMPLER_YCBCR_MODEL_CONVERSION_YCBCR_601_KHR;
-            break;
-        case SDL_MATRIX_COEFFICIENTS_BT709:
-            samplerYcbcrConversionCreateInfo->ycbcrModel = VK_SAMPLER_YCBCR_MODEL_CONVERSION_YCBCR_709_KHR;
-            break;
-        case SDL_MATRIX_COEFFICIENTS_BT2020_NCL:
-            samplerYcbcrConversionCreateInfo->ycbcrModel = VK_SAMPLER_YCBCR_MODEL_CONVERSION_YCBCR_2020_KHR;
-            break;
-        case SDL_MATRIX_COEFFICIENTS_UNSPECIFIED:
-            if (texture->format == SDL_PIXELFORMAT_P010) {
-                samplerYcbcrConversionCreateInfo->ycbcrModel = VK_SAMPLER_YCBCR_MODEL_CONVERSION_YCBCR_2020_KHR;
-            } else if (height > YUV_SD_THRESHOLD) {
-                samplerYcbcrConversionCreateInfo->ycbcrModel = VK_SAMPLER_YCBCR_MODEL_CONVERSION_YCBCR_709_KHR;
-            } else {
-                samplerYcbcrConversionCreateInfo->ycbcrModel = VK_SAMPLER_YCBCR_MODEL_CONVERSION_YCBCR_601_KHR;
-            }
-            break;
-        default:
-            return SDL_SetError("Unsupported Ycbcr colorspace: %d", SDL_COLORSPACEMATRIX(texture->colorspace));
-        }
-        samplerYcbcrConversionCreateInfo->components.r = VK_COMPONENT_SWIZZLE_IDENTITY;
-        samplerYcbcrConversionCreateInfo->components.g = VK_COMPONENT_SWIZZLE_IDENTITY;
-        samplerYcbcrConversionCreateInfo->components.b = VK_COMPONENT_SWIZZLE_IDENTITY;
-        samplerYcbcrConversionCreateInfo->components.a = VK_COMPONENT_SWIZZLE_IDENTITY;
         if (texture->format == SDL_PIXELFORMAT_YV12 ||
-            texture->format == SDL_PIXELFORMAT_NV21) {
-            samplerYcbcrConversionCreateInfo->components.r = VK_COMPONENT_SWIZZLE_B;
-            samplerYcbcrConversionCreateInfo->components.b = VK_COMPONENT_SWIZZLE_R;
+            texture->format == SDL_PIXELFORMAT_IYUV ||
+            texture->format == SDL_PIXELFORMAT_I0FL) {
+            // YUV 4:2:0 formats
+            chromaWidth = (width / 2);
+            chromaHeight = (height / 2);
         }
 
-        switch (SDL_COLORSPACERANGE(texture->colorspace)) {
-        case SDL_COLOR_RANGE_LIMITED:
-            samplerYcbcrConversionCreateInfo->ycbcrRange = VK_SAMPLER_YCBCR_RANGE_ITU_NARROW_KHR;
-            break;
-        case SDL_COLOR_RANGE_FULL:
-        default:
-            samplerYcbcrConversionCreateInfo->ycbcrRange = VK_SAMPLER_YCBCR_RANGE_ITU_FULL_KHR;
-            break;
-        }
-
-        switch (SDL_COLORSPACECHROMA(texture->colorspace)) {
-        case SDL_CHROMA_LOCATION_LEFT:
-            samplerYcbcrConversionCreateInfo->xChromaOffset = VK_CHROMA_LOCATION_COSITED_EVEN_KHR;
-            samplerYcbcrConversionCreateInfo->yChromaOffset = VK_CHROMA_LOCATION_MIDPOINT_KHR;
+        int bits_per_pixel;
+        switch (texture->format) {
+        case SDL_PIXELFORMAT_I0FL:
+        case SDL_PIXELFORMAT_I4FL:
+            bits_per_pixel = 16;
             break;
-        case SDL_CHROMA_LOCATION_TOPLEFT:
-            samplerYcbcrConversionCreateInfo->xChromaOffset = VK_CHROMA_LOCATION_COSITED_EVEN_KHR;
-            samplerYcbcrConversionCreateInfo->yChromaOffset = VK_CHROMA_LOCATION_COSITED_EVEN_KHR;
+        case SDL_PIXELFORMAT_P010:
+            bits_per_pixel = 10;
             break;
-        case SDL_CHROMA_LOCATION_NONE:
-        case SDL_CHROMA_LOCATION_CENTER:
         default:
-            samplerYcbcrConversionCreateInfo->xChromaOffset = VK_CHROMA_LOCATION_MIDPOINT_KHR;
-            samplerYcbcrConversionCreateInfo->yChromaOffset = VK_CHROMA_LOCATION_MIDPOINT_KHR;
+            bits_per_pixel = 8;
             break;
         }
-        samplerYcbcrConversionCreateInfo->chromaFilter = VK_FILTER_LINEAR;
-
-        textureData->yuvPipeline = VULKAN_GetYUVPipeline(rendererData, &createInfo);
-        if (!textureData->yuvPipeline) {
-            return false;
+        textureData->YCbCr_matrix = SDL_GetYCbCRtoRGBConversionMatrix(texture->colorspace, texture->w, texture->h, bits_per_pixel);
+        if (!textureData->YCbCr_matrix) {
+            return SDL_SetError("Unsupported YUV colorspace");
         }
-
-        textureData->yuv = true;
     }
     textureData->width = width;
     textureData->height = height;
 
-    VkImageUsageFlags usage = VK_IMAGE_USAGE_SAMPLED_BIT;
-    if (!external) {
-        usage |= (VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT);
-    }
+    VkImageUsageFlags usage = (VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT);
     if (texture->access == SDL_TEXTUREACCESS_TARGET) {
-        usage |= VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
+        usage |= (VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT);
     }
+    usage |= (VkImageUsageFlags)SDL_GetNumberProperty(create_props, SDL_PROP_TEXTURE_CREATE_VULKAN_USAGE_NUMBER, 0);
+
+    static const char *imageCreateProperties[] = {
+        SDL_PROP_TEXTURE_CREATE_VULKAN_TEXTURE_NUMBER,
+        SDL_PROP_TEXTURE_CREATE_VULKAN_TEXTURE_U_NUMBER,
+        SDL_PROP_TEXTURE_CREATE_VULKAN_TEXTURE_V_NUMBER
+    };
+    SDL_COMPILE_TIME_ASSERT(imageCreateProperties, SDL_arraysize(imageCreateProperties) == SDL_VULKAN_NUM_TEXTURE_BINDINGS);
 
     VkSamplerYcbcrConversionKHR samplerYcbcrConversion = (textureData->yuvPipeline ? textureData->yuvPipeline->samplerYcbcrConversion : VK_NULL_HANDLE);
-    result = VULKAN_AllocateImage(rendererData, create_props, width, height, textureFormat, usage, imageViewSwizzle, samplerYcbcrConversion, external, &textureData->mainImage);
-    if (result != VK_SUCCESS) {
-        SET_ERROR_CODE("VULKAN_AllocateImage()", result);
-        return false;
+    for (int i = 0; i < numImages; ++i) {
+        uint32_t imageWidth = (i == 0 ? width : chromaWidth);
+        uint32_t imageHeight = (i == 0 ? height : chromaHeight);
+        result = VULKAN_AllocateImage(rendererData, create_props, imageCreateProperties[i], imageWidth, imageHeight, textureFormat, usage, samplerYcbcrConversion, external, &tex

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