SDL: Added support for using Android hardware buffers with the Vulkan renderer

From 982333a48dee3acf276d5322e7d4e5eef933f153 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Fri, 11 Sep 2026 11:12:12 -0700
Subject: [PATCH] Added support for using Android hardware buffers with the
 Vulkan renderer

---
 include/SDL3/SDL_render.h             |   4 +
 src/render/vulkan/SDL_render_vulkan.c | 433 +++++++++++++++++++++-----
 2 files changed, 358 insertions(+), 79 deletions(-)

diff --git a/include/SDL3/SDL_render.h b/include/SDL3/SDL_render.h
index 2962cde4aa5c8..8baaf143d32f4 100644
--- a/include/SDL3/SDL_render.h
+++ b/include/SDL3/SDL_render.h
@@ -806,6 +806,9 @@ extern SDL_DECLSPEC SDL_Texture * SDLCALL SDL_CreateTextureFromSurface(SDL_Rende
  *   with the 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_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.
  *
  * With the GPU renderer:
  *
@@ -869,6 +872,7 @@ extern SDL_DECLSPEC SDL_Texture * SDLCALL SDL_CreateTextureWithProperties(SDL_Re
 #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_LAYOUT_NUMBER            "SDL.texture.create.vulkan.layout"
+#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"
 #define SDL_PROP_TEXTURE_CREATE_GPU_TEXTURE_U_POINTER           "SDL.texture.create.gpu.texture_u"
diff --git a/src/render/vulkan/SDL_render_vulkan.c b/src/render/vulkan/SDL_render_vulkan.c
index f69bfc562c455..aaa655b739e53 100644
--- a/src/render/vulkan/SDL_render_vulkan.c
+++ b/src/render/vulkan/SDL_render_vulkan.c
@@ -40,6 +40,13 @@
 #include "../../video/SDL_yuv_c.h"
 #include "SDL_shaders_vulkan.h"
 
+#ifdef SDL_PLATFORM_ANDROID
+#include <dlfcn.h>
+#include <android/hardware_buffer.h>
+
+typedef void (*pfnAHardwareBuffer_describe)(const AHardwareBuffer *, AHardwareBuffer_Desc *);
+#endif
+
 #define SET_ERROR_CODE(message, rc)                                                                 \
     if (SDL_GetHintBoolean(SDL_HINT_RENDER_VULKAN_DEBUG, false)) {                                  \
         SDL_LogError(SDL_LOG_CATEGORY_RENDER, "%s: %s", message, SDL_Vulkan_GetResultString(rc)); \
@@ -54,6 +61,12 @@
     }                                                                                               \
     SDL_SetError("%s", message)                                                                     \
 
+#ifdef SDL_PLATFORM_ANDROID
+#define ANDROID_OPTIONAL_DEVICE_FUNCTION(X)    VULKAN_OPTIONAL_DEVICE_FUNCTION(X)
+#else
+#define ANDROID_OPTIONAL_DEVICE_FUNCTION(X)
+#endif
+
 #define VULKAN_FUNCTIONS()                                              \
     VULKAN_DEVICE_FUNCTION(vkAcquireNextImageKHR)                       \
     VULKAN_DEVICE_FUNCTION(vkAllocateCommandBuffers)                    \
@@ -150,6 +163,7 @@
     VULKAN_OPTIONAL_INSTANCE_FUNCTION(vkGetPhysicalDeviceProperties2KHR)            \
     VULKAN_OPTIONAL_DEVICE_FUNCTION(vkCreateSamplerYcbcrConversionKHR)              \
     VULKAN_OPTIONAL_DEVICE_FUNCTION(vkDestroySamplerYcbcrConversionKHR)             \
+    ANDROID_OPTIONAL_DEVICE_FUNCTION(vkGetAndroidHardwareBufferPropertiesANDROID)   \
 
 #define VULKAN_DEVICE_FUNCTION(name)            static PFN_##name name = NULL;
 #define VULKAN_GLOBAL_FUNCTION(name)            static PFN_##name name = NULL;
@@ -221,6 +235,19 @@ typedef struct
     SDL_FColor color;
 } VULKAN_VertexPositionColor;
 
+// Vulkan OES buffer info
+typedef struct
+{
+#ifdef SDL_PLATFORM_ANDROID
+    struct AHardwareBuffer *buffer;
+    VkAndroidHardwareBufferPropertiesANDROID properties;
+    VkAndroidHardwareBufferFormatPropertiesANDROID formatProperties;
+    VkExternalFormatANDROID externalFormat;
+#else
+    int dummy;
+#endif
+} VULKAN_ExternalBufferInfo;
+
 // Vulkan Buffer
 typedef struct
 {
@@ -249,10 +276,17 @@ typedef struct
 } VULKAN_PaletteData;
 
 // YUV conversion data
+typedef struct
+{
+    Uint64 externalFormat;
+    VkFilter filter;
+    VkSamplerYcbcrConversionCreateInfoKHR samplerYcbcrConversionCreateInfo;
+} VULKAN_YUVPipelineCreateInfo;
+
 typedef struct VULKAN_YUVPipeline
 {
     // The information used to create this entry
-    VkSamplerYcbcrConversionCreateInfoKHR samplerYcbcrConversionCreateInfo;
+    VULKAN_YUVPipelineCreateInfo createInfo;
 
     // Object passed to VkImageView and VkSampler for doing Ycbcr -> RGB conversion
     VkSamplerYcbcrConversion samplerYcbcrConversion;
@@ -374,6 +408,8 @@ typedef struct
     bool supportsEXTSwapchainColorspace;
     bool supportsKHRGetPhysicalDeviceProperties2;
     bool supportsKHRSamplerYCbCrConversion;
+    bool supportsKHRExternalMemoryCapabilities;
+    bool supportsANDROIDExternalMemoryAndroidHardwareBuffer;
     uint32_t surfaceFormatsAllocatedCount;
     uint32_t surfaceFormatsCount;
     uint32_t swapchainDesiredImageCount;
@@ -532,13 +568,14 @@ static void VULKAN_DestroyBuffer(VULKAN_RenderData *rendererData, VULKAN_Buffer
 static void VULKAN_DestroyImage(VULKAN_RenderData *rendererData, VULKAN_Image *vulkanImage);
 static void VULKAN_ResetCommandList(VULKAN_RenderData *rendererData);
 static void VULKAN_EnsureCommandBuffer(VULKAN_RenderData *rendererData);
+static void VULKAN_RecordExternalImageBarrier(VULKAN_RenderData *rendererData, VkAccessFlags sourceAccessMask, VkAccessFlags destAccessMask, VkPipelineStageFlags srcStageFlags, VkPipelineStageFlags dstStageFlags, VkImageLayout destLayout, VkImage image, VkImageLayout *imageLayout);
 static void VULKAN_RecordPipelineImageBarrier(VULKAN_RenderData *rendererData, VkAccessFlags sourceAccessMask, VkAccessFlags destAccessMask, VkPipelineStageFlags srcStageFlags, VkPipelineStageFlags dstStageFlags, VkImageLayout destLayout, VkImage image, VkImageLayout *imageLayout);
 static bool VULKAN_FindMemoryTypeIndex(VULKAN_RenderData *rendererData, uint32_t typeBits, VkMemoryPropertyFlags requiredFlags, VkMemoryPropertyFlags desiredFlags, uint32_t *memoryTypeIndexOut);
 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);
-static VULKAN_YUVPipeline *VULKAN_GetYUVPipeline(VULKAN_RenderData *rendererData, const VkSamplerYcbcrConversionCreateInfoKHR *samplerYcbcrConversionCreateInfo);
-static void VULKAN_CleanupYUVPipeline(VULKAN_RenderData *rendererData, VULKAN_YUVPipeline *data);
+static VULKAN_YUVPipeline *VULKAN_GetYUVPipeline(VULKAN_RenderData *rendererData, const VULKAN_YUVPipelineCreateInfo *createInfo);
+static void VULKAN_CleanupYUVPipeline(VULKAN_RenderData *rendererData, VULKAN_YUVPipeline *pipeline);
 static VkSurfaceTransformFlagBitsKHR VULKAN_GetRotationForCurrentRenderTarget(VULKAN_RenderData *rendererData);
 static bool VULKAN_IsDisplayRotated90Degrees(VkSurfaceTransformFlagBitsKHR rotation);
 
@@ -837,7 +874,7 @@ static void VULKAN_DestroyImage(VULKAN_RenderData *rendererData, VULKAN_Image *v
     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_Image *imageOut)
+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)
 {
     VkResult result;
     VkSamplerYcbcrConversionInfoKHR samplerYcbcrConversionInfo = { 0 };
@@ -866,8 +903,18 @@ static VkResult VULKAN_AllocateImage(VULKAN_RenderData *rendererData, SDL_Proper
         imageCreateInfo.queueFamilyIndexCount = 0;
         imageCreateInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
 
+#ifdef SDL_PLATFORM_ANDROID
+        VkExternalMemoryImageCreateInfoKHR externalMemoryImageCreateInfo = { 0 };
+        if (external) {
+            externalMemoryImageCreateInfo.sType = VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO_KHR;
+            externalMemoryImageCreateInfo.pNext = &external->externalFormat;
+            externalMemoryImageCreateInfo.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_ANDROID_HARDWARE_BUFFER_BIT_ANDROID;
+            imageCreateInfo.pNext = &externalMemoryImageCreateInfo;
+        }
+#endif
+
         // Allow writing to planar YUV textures from application code
-        if (VULKAN_VkFormatGetNumPlanes(format) > 1) {
+        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;
         }
@@ -880,11 +927,19 @@ static VkResult VULKAN_AllocateImage(VULKAN_RenderData *rendererData, SDL_Proper
         }
 
         VkMemoryRequirements memoryRequirements = { 0 };
-        vkGetImageMemoryRequirements(rendererData->device, imageOut->image, &memoryRequirements);
-        if (result != VK_SUCCESS) {
-            VULKAN_DestroyImage(rendererData, imageOut);
-            SET_ERROR_CODE("vkGetImageMemoryRequirements()", result);
-            return result;
+#ifdef SDL_PLATFORM_ANDROID
+        if (external) {
+            memoryRequirements.memoryTypeBits = external->properties.memoryTypeBits;
+            memoryRequirements.size = external->properties.allocationSize;
+        } else
+#endif
+        {
+            vkGetImageMemoryRequirements(rendererData->device, imageOut->image, &memoryRequirements);
+            if (result != VK_SUCCESS) {
+                VULKAN_DestroyImage(rendererData, imageOut);
+                SET_ERROR_CODE("vkGetImageMemoryRequirements()", result);
+                return result;
+            }
         }
 
         uint32_t memoryTypeIndex = 0;
@@ -897,6 +952,21 @@ static VkResult VULKAN_AllocateImage(VULKAN_RenderData *rendererData, SDL_Proper
         memoryAllocateInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
         memoryAllocateInfo.allocationSize = memoryRequirements.size;
         memoryAllocateInfo.memoryTypeIndex = memoryTypeIndex;
+
+#ifdef SDL_PLATFORM_ANDROID
+        VkImportAndroidHardwareBufferInfoANDROID importHardwareBufferInfo = { 0 };
+        VkMemoryDedicatedAllocateInfoKHR dedicatedAllocateInfo = { 0 };
+        if (external) {
+            importHardwareBufferInfo.sType = VK_STRUCTURE_TYPE_IMPORT_ANDROID_HARDWARE_BUFFER_INFO_ANDROID;
+            importHardwareBufferInfo.buffer = external->buffer;
+
+            dedicatedAllocateInfo.sType = VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO_KHR;
+            dedicatedAllocateInfo.pNext = &importHardwareBufferInfo;
+            dedicatedAllocateInfo.image = imageOut->image;
+
+            memoryAllocateInfo.pNext = &dedicatedAllocateInfo;
+        }
+#endif
         result = vkAllocateMemory(rendererData->device, &memoryAllocateInfo, NULL, &imageOut->deviceMemory);
         if (result != VK_SUCCESS) {
             VULKAN_DestroyImage(rendererData, imageOut);
@@ -915,14 +985,25 @@ static VkResult VULKAN_AllocateImage(VULKAN_RenderData *rendererData, SDL_Proper
 
     if (imageOut->imageLayout != VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) {
         VULKAN_EnsureCommandBuffer(rendererData);
-        VULKAN_RecordPipelineImageBarrier(rendererData,
-            VK_ACCESS_NONE,
-            VK_ACCESS_SHADER_READ_BIT,
-            VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
-            VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
-            VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
-            imageOut->image,
-            &imageOut->imageLayout);
+        if (external) {
+            VULKAN_RecordExternalImageBarrier(rendererData,
+                VK_ACCESS_NONE,
+                VK_ACCESS_SHADER_READ_BIT,
+                VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
+                VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
+                VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
+                imageOut->image,
+                &imageOut->imageLayout);
+        } else {
+            VULKAN_RecordPipelineImageBarrier(rendererData,
+                VK_ACCESS_NONE,
+                VK_ACCESS_SHADER_READ_BIT,
+                VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
+                VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
+                VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
+                imageOut->image,
+                &imageOut->imageLayout);
+        }
     }
 
     VkImageViewCreateInfo imageViewCreateInfo = { 0 };
@@ -960,6 +1041,33 @@ static VkResult VULKAN_AllocateImage(VULKAN_RenderData *rendererData, SDL_Proper
 }
 
 
+static void VULKAN_RecordExternalImageBarrier(VULKAN_RenderData *rendererData, VkAccessFlags sourceAccessMask, VkAccessFlags destAccessMask,
+    VkPipelineStageFlags srcStageFlags, VkPipelineStageFlags dstStageFlags, VkImageLayout destLayout, VkImage image, VkImageLayout *imageLayout)
+{
+    // Stop any outstanding renderpass if open
+    if (rendererData->currentRenderPass != VK_NULL_HANDLE) {
+        vkCmdEndRenderPass(rendererData->currentCommandBuffer);
+        rendererData->currentRenderPass = VK_NULL_HANDLE;
+    }
+
+    VkImageMemoryBarrier barrier = { 0 };
+    barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
+    barrier.srcAccessMask = sourceAccessMask;
+    barrier.dstAccessMask = destAccessMask;
+    barrier.oldLayout = *imageLayout;
+    barrier.newLayout = destLayout;
+    barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_FOREIGN_EXT;
+    barrier.dstQueueFamilyIndex = rendererData->graphicsQueueFamilyIndex;
+    barrier.image = image;
+    barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
+    barrier.subresourceRange.baseMipLevel = 0;
+    barrier.subresourceRange.levelCount = 1;
+    barrier.subresourceRange.baseArrayLayer = 0;
+    barrier.subresourceRange.layerCount = 1;
+    vkCmdPipelineBarrier(rendererData->currentCommandBuffer, srcStageFlags, dstStageFlags, 0, 0, NULL, 0, NULL, 1, &barrier);
+    *imageLayout = destLayout;
+}
+
 static void VULKAN_RecordPipelineImageBarrier(VULKAN_RenderData *rendererData, VkAccessFlags sourceAccessMask, VkAccessFlags destAccessMask,
     VkPipelineStageFlags srcStageFlags, VkPipelineStageFlags dstStageFlags, VkImageLayout destLayout, VkImage image, VkImageLayout *imageLayout)
 {
@@ -1796,18 +1904,34 @@ static bool VULKAN_ValidationLayersFound(void)
 // Create resources that depend on the device.
 static VkResult VULKAN_CreateDeviceResources(SDL_Renderer *renderer, SDL_PropertiesID create_props)
 {
+    /* The list of extension names, in dependency order.
+     * If there are extension blocks that are not dependent upon each other, you'll need
+     * to dynamically build the list of extension names instead of using this array.
+     */
     static const char *const deviceExtensionNames[] = {
         VK_KHR_SWAPCHAIN_EXTENSION_NAME,
-        /* VK_KHR_sampler_ycbcr_conversion + dependent extensions.
-           Note VULKAN_DeviceExtensionsFound() call below, if these get moved in this
-           array, update that check too.
-       */
+        // VK_KHR_sampler_ycbcr_conversion + dependent extensions
+#define YCBCR_EXTENSION_OFFSET 1
+#define YCBCR_EXTENSION_COUNT 5
         VK_KHR_SAMPLER_YCBCR_CONVERSION_EXTENSION_NAME,
         VK_KHR_MAINTENANCE1_EXTENSION_NAME,
         VK_KHR_MAINTENANCE2_EXTENSION_NAME,
         VK_KHR_BIND_MEMORY_2_EXTENSION_NAME,
         VK_KHR_GET_MEMORY_REQUIREMENTS_2_EXTENSION_NAME,
+#ifdef SDL_PLATFORM_ANDROID
+        // VK_ANDROID_external_memory_android_hardware_buffer + dependent extensions
+#define ANDROID_EXTENSION_OFFSET (YCBCR_EXTENSION_OFFSET + YCBCR_EXTENSION_COUNT)
+#define ANDROID_EXTENSION_COUNT 4
+        VK_ANDROID_EXTERNAL_MEMORY_ANDROID_HARDWARE_BUFFER_EXTENSION_NAME,
+        VK_KHR_EXTERNAL_MEMORY_EXTENSION_NAME,
+        VK_KHR_DEDICATED_ALLOCATION_EXTENSION_NAME,
+        VK_EXT_QUEUE_FAMILY_FOREIGN_EXTENSION_NAME,
+#else
+#define ANDROID_EXTENSION_COUNT 0
+#endif
     };
+    SDL_COMPILE_TIME_ASSERT(deviceExtensionNames, SDL_arraysize(deviceExtensionNames) == (YCBCR_EXTENSION_OFFSET + YCBCR_EXTENSION_COUNT + ANDROID_EXTENSION_COUNT));
+
     VULKAN_RenderData *rendererData = (VULKAN_RenderData *)renderer->internal;
     VkResult result = VK_SUCCESS;
     PFN_vkGetInstanceProcAddr vkGetInstanceProcAddr = NULL;
@@ -1844,6 +1968,9 @@ static VkResult VULKAN_CreateDeviceResources(SDL_Renderer *renderer, SDL_Propert
     // Check for VK_KHR_get_physical_device_properties2
     rendererData->supportsKHRGetPhysicalDeviceProperties2 = VULKAN_InstanceExtensionFound(rendererData, VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME);
 
+    // Check for VK_KHR_external_memory_capabilities, which we need to import hardware buffers
+    rendererData->supportsKHRExternalMemoryCapabilities = VULKAN_InstanceExtensionFound(rendererData, VK_KHR_EXTERNAL_MEMORY_CAPABILITIES_EXTENSION_NAME);
+
     // Create VkInstance
     rendererData->instance = (VkInstance)SDL_GetPointerProperty(create_props, SDL_PROP_RENDERER_CREATE_VULKAN_INSTANCE_POINTER, NULL);
     if (rendererData->instance) {
@@ -1857,7 +1984,7 @@ static VkResult VULKAN_CreateDeviceResources(SDL_Renderer *renderer, SDL_Propert
         instanceCreateInfo.pApplicationInfo = &appInfo;
         char const *const *instanceExtensions = SDL_Vulkan_GetInstanceExtensions(&instanceCreateInfo.enabledExtensionCount);
 
-        const char **instanceExtensionsCopy = (const char **)SDL_calloc(instanceCreateInfo.enabledExtensionCount + 2, sizeof(const char *));
+        const char **instanceExtensionsCopy = (const char **)SDL_calloc(instanceCreateInfo.enabledExtensionCount + 3, sizeof(const char *));
         for (uint32_t i = 0; i < instanceCreateInfo.enabledExtensionCount; i++) {
             instanceExtensionsCopy[i] = instanceExtensions[i];
         }
@@ -1869,6 +1996,10 @@ static VkResult VULKAN_CreateDeviceResources(SDL_Renderer *renderer, SDL_Propert
             instanceExtensionsCopy[instanceCreateInfo.enabledExtensionCount] = VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME;
             instanceCreateInfo.enabledExtensionCount++;
         }
+        if (rendererData->supportsKHRExternalMemoryCapabilities) {
+            instanceExtensionsCopy[instanceCreateInfo.enabledExtensionCount] = VK_KHR_EXTERNAL_MEMORY_CAPABILITIES_EXTENSION_NAME;
+            instanceCreateInfo.enabledExtensionCount++;
+        }
         instanceCreateInfo.ppEnabledExtensionNames = (const char *const *)instanceExtensionsCopy;
         if (createDebug && VULKAN_ValidationLayersFound()) {
             instanceCreateInfo.ppEnabledLayerNames = validationLayerName;
@@ -1920,10 +2051,18 @@ static VkResult VULKAN_CreateDeviceResources(SDL_Renderer *renderer, SDL_Propert
     }
 
     if (rendererData->supportsKHRGetPhysicalDeviceProperties2 &&
-        VULKAN_DeviceExtensionsFound(rendererData, 4, &deviceExtensionNames[1])) {
+        VULKAN_DeviceExtensionsFound(rendererData, YCBCR_EXTENSION_COUNT, &deviceExtensionNames[YCBCR_EXTENSION_OFFSET])) {
         rendererData->supportsKHRSamplerYCbCrConversion = true;
     }
 
+#ifdef SDL_PLATFORM_ANDROID
+    if (rendererData->supportsKHRSamplerYCbCrConversion &&
+        rendererData->supportsKHRExternalMemoryCapabilities &&
+        VULKAN_DeviceExtensionsFound(rendererData, ANDROID_EXTENSION_COUNT, &deviceExtensionNames[ANDROID_EXTENSION_OFFSET])) {
+        rendererData->supportsANDROIDExternalMemoryAndroidHardwareBuffer = true;
+    }
+#endif
+
     // Create Vulkan device
     rendererData->device = (VkDevice)SDL_GetPointerProperty(create_props, SDL_PROP_RENDERER_CREATE_VULKAN_DEVICE_POINTER, NULL);
     if (rendererData->device) {
@@ -1938,7 +2077,15 @@ static VkResult VULKAN_CreateDeviceResources(SDL_Renderer *renderer, SDL_Propert
         deviceCreateInfo.queueCreateInfoCount = 0;
         deviceCreateInfo.pQueueCreateInfos = deviceQueueCreateInfo;
         deviceCreateInfo.pEnabledFeatures = NULL;
-        deviceCreateInfo.enabledExtensionCount = (rendererData->supportsKHRSamplerYCbCrConversion) ? SDL_arraysize(deviceExtensionNames) : 1;
+        deviceCreateInfo.enabledExtensionCount = 1;
+        if (rendererData->supportsKHRSamplerYCbCrConversion) {
+            deviceCreateInfo.enabledExtensionCount += YCBCR_EXTENSION_COUNT;
+        }
+#ifdef SDL_PLATFORM_ANDROID
+        if (rendererData->supportsANDROIDExternalMemoryAndroidHardwareBuffer) {
+            deviceCreateInfo.enabledExtensionCount += ANDROID_EXTENSION_COUNT;
+        }
+#endif
         deviceCreateInfo.ppEnabledExtensionNames = deviceExtensionNames;
 
         deviceQueueCreateInfo[0].sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
@@ -1975,6 +2122,12 @@ static VkResult VULKAN_CreateDeviceResources(SDL_Renderer *renderer, SDL_Propert
         return VK_ERROR_UNKNOWN;
     }
 
+#ifdef SDL_PLATFORM_ANDROID
+    if (!vkGetAndroidHardwareBufferPropertiesANDROID) {
+        rendererData->supportsANDROIDExternalMemoryAndroidHardwareBuffer = false;
+    }
+#endif
+
     // Get graphics/present queues
     vkGetDeviceQueue(rendererData->device, rendererData->graphicsQueueFamilyIndex, 0, &rendererData->graphicsQueue);
     if (rendererData->graphicsQueueFamilyIndex != rendererData->presentQueueFamilyIndex) {
@@ -2644,7 +2797,7 @@ 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, &palettedata->image);
+    VkResult result = VULKAN_AllocateImage(data, 0, 256, 1, format, usage, imageViewSwizzle, VK_NULL_HANDLE, NULL, &palettedata->image);
     if (result != VK_SUCCESS) {
         SET_ERROR_CODE("VULKAN_AllocateImage()", result);
         return false;
@@ -2678,6 +2831,30 @@ static void VULKAN_DestroyPalette(SDL_Renderer *renderer, SDL_TexturePalette *pa
     SDL_free(palettedata);
 }
 
+#ifdef SDL_PLATFORM_ANDROID
+static void ANDROID_GetBufferSize(struct AHardwareBuffer *buffer, uint32_t *width, uint32_t *height)
+{
+    static pfnAHardwareBuffer_describe AHardwareBuffer_describeFunc;
+    if (!AHardwareBuffer_describeFunc) {
+        void *lib = dlopen("libandroid.so", RTLD_NOW | RTLD_LOCAL);
+        if (lib) {
+            AHardwareBuffer_describeFunc = (pfnAHardwareBuffer_describe)dlsym(lib, "AHardwareBuffer_describe");
+
+            // Leaving the library loaded so we can use the function again without another dlopen()
+        }
+    }
+    if (AHardwareBuffer_describeFunc) {
+        AHardwareBuffer_Desc desc;
+        SDL_zero(desc);
+        AHardwareBuffer_describeFunc(buffer, &desc);
+        if (desc.width > 0 && desc.height > 0) {
+            *width = desc.width;
+            *height = desc.height;
+        }
+    }
+}
+#endif // SDL_PLATFORM_ANDROID
+
 static bool VULKAN_CreateTexture(SDL_Renderer *renderer, SDL_Texture *texture, SDL_PropertiesID create_props)
 {
     VULKAN_RenderData *rendererData = (VULKAN_RenderData *)renderer->internal;
@@ -2687,12 +2864,14 @@ static bool VULKAN_CreateTexture(SDL_Renderer *renderer, SDL_Texture *texture, S
     uint32_t width = texture->w;
     uint32_t height = texture->h;
     VkComponentMapping imageViewSwizzle = rendererData->identitySwizzle;
+    VULKAN_ExternalBufferInfo externalBufferInfo;
+    VULKAN_ExternalBufferInfo *external = NULL;
 
     if (!rendererData->device) {
         return SDL_SetError("Device lost and couldn't be recovered");
     }
 
-    if (textureFormat == VK_FORMAT_UNDEFINED) {
+    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);
     }
 
@@ -2702,25 +2881,95 @@ static bool VULKAN_CreateTexture(SDL_Renderer *renderer, SDL_Texture *texture, S
     }
     texture->internal = textureData;
 
-    // YUV textures must have even width and height.  Also create Ycbcr conversion
-    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;
+    if (texture->format == SDL_PIXELFORMAT_EXTERNAL_OES) {
+        SDL_zero(externalBufferInfo);
+        external = &externalBufferInfo;
 
-        // Check that we have VK_KHR_sampler_ycbcr_conversion support
-        if (!rendererData->supportsKHRSamplerYCbCrConversion) {
-            return SDL_SetError("YUV textures require a Vulkan device that supports VK_KHR_sampler_ycbcr_conversion");
+        if (texture->access == SDL_TEXTUREACCESS_TARGET) {
+            return SDL_SetError("SDL_PIXELFORMAT_EXTERNAL_OES textures can't be render targets");
         }
 
-        VkSamplerYcbcrConversionCreateInfoKHR samplerYcbcrConversionCreateInfo;
-        SDL_zero(samplerYcbcrConversionCreateInfo);
-        samplerYcbcrConversionCreateInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_YCBCR_CONVERSION_CREATE_INFO_KHR;
+#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) {
+            return SDL_SetError("SDL_PIXELFORMAT_EXTERNAL_OES texture requires a hardware buffer");
+        }
+
+        // Get the real buffer width and height
+        ANDROID_GetBufferSize(external->buffer, &width, &height);
+
+        VkAndroidHardwareBufferFormatPropertiesANDROID *formatProperties = &external->formatProperties;
+        formatProperties->sType = VK_STRUCTURE_TYPE_ANDROID_HARDWARE_BUFFER_FORMAT_PROPERTIES_ANDROID;
+
+        VkAndroidHardwareBufferPropertiesANDROID *properties = &external->properties;
+        properties->sType = VK_STRUCTURE_TYPE_ANDROID_HARDWARE_BUFFER_PROPERTIES_ANDROID;
+        properties->pNext = formatProperties;
+        result = vkGetAndroidHardwareBufferPropertiesANDROID(rendererData->device, external->buffer, properties);
+        if (result != VK_SUCCESS) {
+            SET_ERROR_CODE("vkGetAndroidHardwareBufferPropertiesANDROID()", result);
+            return false;
+        }
+
+        VkExternalFormatANDROID *externalFormat = &external->externalFormat;
+        externalFormat->sType = VK_STRUCTURE_TYPE_EXTERNAL_FORMAT_ANDROID;
+        externalFormat->externalFormat = formatProperties->externalFormat;
+
+        textureFormat = formatProperties->format;
+
+        if (textureFormat == VK_FORMAT_UNDEFINED ||
+            formatProperties->suggestedYcbcrModel != VK_SAMPLER_YCBCR_MODEL_CONVERSION_RGB_IDENTITY_KHR) {
+            VkFilter chromaFilter = VK_FILTER_LINEAR;
+            VkFilter lumaFilter = VK_FILTER_LINEAR;
+            if (!(formatProperties->formatFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_YCBCR_CONVERSION_LINEAR_FILTER_BIT)) {
+                chromaFilter = VK_FILTER_NEAREST;
+            }
+            if (!(formatProperties->formatFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT)) {
+                lumaFilter = VK_FILTER_NEAREST;
+            }
+            if (!(formatProperties->formatFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_YCBCR_CONVERSION_SEPARATE_RECONSTRUCTION_FILTER_BIT)) {
+                lumaFilter = chromaFilter;
+            }
+
+            VULKAN_YUVPipelineCreateInfo createInfo;
+            SDL_zero(createInfo); // Must zero padding bytes for cache
+            createInfo.externalFormat = formatProperties->externalFormat;
+            createInfo.filter = lumaFilter;
+
+            VkSamplerYcbcrConversionCreateInfoKHR *samplerYcbcrConversionCreateInfo = &createInfo.samplerYcbcrConversionCreateInfo;
+            samplerYcbcrConversionCreateInfo->sType = VK_STRUCTURE_TYPE_SAMPLER_YCBCR_CONVERSION_CREATE_INFO_KHR;
+            samplerYcbcrConversionCreateInfo->pNext = externalFormat;
+            samplerYcbcrConversionCreateInfo->format = formatProperties->format;
+            samplerYcbcrConversionCreateInfo->ycbcrModel = formatProperties->suggestedYcbcrModel;
+            samplerYcbcrConversionCreateInfo->ycbcrRange = formatProperties->suggestedYcbcrRange;
+            samplerYcbcrConversionCreateInfo->components = formatProperties->samplerYcbcrConversionComponents;
+            samplerYcbcrConversionCreateInfo->xChromaOffset = formatProperties->suggestedXChromaOffset;
+            samplerYcbcrConversionCreateInfo->yChromaOffset = formatProperties->suggestedYChromaOffset;
+            samplerYcbcrConversionCreateInfo->chromaFilter = chromaFilter;
+
+            textureData->yuvPipeline = VULKAN_GetYUVPipeline(rendererData, &createInfo);
+            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) {
@@ -2730,70 +2979,69 @@ static bool VULKAN_CreateTexture(SDL_Renderer *renderer, SDL_Texture *texture, S
         }
 
         // Creat

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