SDL: Added SDL_AddVulkanRenderSemaphores() for external synchronization with SDL rendering

From 48471f7dbd96365dd5471d06a92275a80954667d Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Sat, 2 Mar 2024 10:03:37 -0800
Subject: [PATCH] Added SDL_AddVulkanRenderSemaphores() for external
 synchronization with SDL rendering

---
 include/SDL3/SDL_render.h             | 21 ++++++
 src/dynapi/SDL_dynapi.sym             |  1 +
 src/dynapi/SDL_dynapi_overrides.h     |  1 +
 src/dynapi/SDL_dynapi_procs.h         |  1 +
 src/render/SDL_render.c               | 10 +++
 src/render/SDL_sysrender.h            |  2 +
 src/render/vulkan/SDL_render_vulkan.c | 98 +++++++++++++++++++++++++--
 7 files changed, 129 insertions(+), 5 deletions(-)

diff --git a/include/SDL3/SDL_render.h b/include/SDL3/SDL_render.h
index 1e290e1ca1ce1..7c99a40db7327 100644
--- a/include/SDL3/SDL_render.h
+++ b/include/SDL3/SDL_render.h
@@ -407,6 +407,7 @@ extern DECLSPEC int SDLCALL SDL_GetRendererInfo(SDL_Renderer *renderer, SDL_Rend
  *   family index used for rendering
  * - `SDL_PROP_RENDERER_VULKAN_PRESENT_QUEUE_FAMILY_INDEX_NUMBER`: the queue
  *   family index used for presentation
+ * - `SDL_PROP_RENDERER_VULKAN_SWAPCHAIN_IMAGE_COUNT_NUMBER`: the number of swapchain images, or potential frames in flight, used by the Vulkan renderer
  *
  * \param renderer the rendering context
  * \returns a valid property ID on success or 0 on failure; call
@@ -436,6 +437,7 @@ extern DECLSPEC SDL_PropertiesID SDLCALL SDL_GetRendererProperties(SDL_Renderer
 #define SDL_PROP_RENDERER_VULKAN_DEVICE_POINTER                     "SDL.renderer.vulkan.device"
 #define SDL_PROP_RENDERER_VULKAN_GRAPHICS_QUEUE_FAMILY_INDEX_NUMBER "SDL.renderer.vulkan.graphics_queue_family_index"
 #define SDL_PROP_RENDERER_VULKAN_PRESENT_QUEUE_FAMILY_INDEX_NUMBER  "SDL.renderer.vulkan.present_queue_family_index"
+#define SDL_PROP_RENDERER_VULKAN_SWAPCHAIN_IMAGE_COUNT_NUMBER       "SDL.renderer.vulkan.swapchain_image_count"
 
 /**
  * Get the output size in pixels of a rendering context.
@@ -2104,6 +2106,25 @@ extern DECLSPEC void *SDLCALL SDL_GetRenderMetalLayer(SDL_Renderer *renderer);
  */
 extern DECLSPEC void *SDLCALL SDL_GetRenderMetalCommandEncoder(SDL_Renderer *renderer);
 
+
+/**
+ * Add a set of synchronization semaphores for the current frame.
+ *
+ * The Vulkan renderer will wait for `wait_semaphore` before submitting rendering commands and signal `signal_semaphore` after rendering commands are complete for this frame.
+ *
+ * This should be called each frame that you want semaphore synchronization. The Vulkan renderer may have multiple frames in flight on the GPU, so you should have multiple semaphores that are used for synchronization. Querying SDL_PROP_RENDERER_VULKAN_SWAPCHAIN_IMAGE_COUNT_NUMBER will give you the maximum number of semaphores you'll need.
+ *
+ * \param renderer the rendering context
+ * \param wait_stage_mask the VkPipelineStageFlags for the wait
+ * \param wait_semaphore a VkSempahore to wait on before rendering the current frame, or 0 if not needed
+ * \param signal_semaphore a VkSempahore that SDL will signal when rendering for the current frame is complete, or 0 if not needed
+ * \returns 0 on success or a negative error code on failure; call
+ *          SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 3.0.0.
+ */
+extern DECLSPEC int SDLCALL SDL_AddVulkanRenderSemaphores(SDL_Renderer *renderer, Uint32 wait_stage_mask, Sint64 wait_semaphore, Sint64 signal_semaphore);
+
 /**
  * Toggle VSync of the given renderer.
  *
diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym
index e351d9f0890ea..5d08979e090cd 100644
--- a/src/dynapi/SDL_dynapi.sym
+++ b/src/dynapi/SDL_dynapi.sym
@@ -973,6 +973,7 @@ SDL3_0.0.0 {
     SDL_GetCameraDevicePosition;
     SDL_qsort_r;
     SDL_bsearch_r;
+    SDL_AddVulkanRenderSemaphores;
     # extra symbols go here (don't modify this line)
   local: *;
 };
diff --git a/src/dynapi/SDL_dynapi_overrides.h b/src/dynapi/SDL_dynapi_overrides.h
index ad8030b6c407c..c5378f1fd77fb 100644
--- a/src/dynapi/SDL_dynapi_overrides.h
+++ b/src/dynapi/SDL_dynapi_overrides.h
@@ -998,3 +998,4 @@
 #define SDL_GetCameraDevicePosition SDL_GetCameraDevicePosition_REAL
 #define SDL_qsort_r SDL_qsort_r_REAL
 #define SDL_bsearch_r SDL_bsearch_r_REAL
+#define SDL_AddVulkanRenderSemaphores SDL_AddVulkanRenderSemaphores_REAL
diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h
index 3971dab5711b3..b19f615cb83f4 100644
--- a/src/dynapi/SDL_dynapi_procs.h
+++ b/src/dynapi/SDL_dynapi_procs.h
@@ -1023,3 +1023,4 @@ SDL_DYNAPI_PROC(int,SDL_GetCameraPermissionState,(SDL_Camera *a),(a),return)
 SDL_DYNAPI_PROC(SDL_CameraPosition,SDL_GetCameraDevicePosition,(SDL_CameraDeviceID a),(a),return)
 SDL_DYNAPI_PROC(void,SDL_qsort_r,(void *a, size_t b, size_t c, int (SDLCALL *d)(void *, const void *, const void *), void *e),(a,b,c,d,e),)
 SDL_DYNAPI_PROC(void*,SDL_bsearch_r,(const void *a, const void *b, size_t c, size_t d, int (SDLCALL *e)(void *, const void *, const void *), void *f),(a,b,c,d,e,f),return)
+SDL_DYNAPI_PROC(int,SDL_AddVulkanRenderSemaphores,(SDL_Renderer *a, Uint32 b, Sint64 c, Sint64 d),(a,b,c,d),return)
diff --git a/src/render/SDL_render.c b/src/render/SDL_render.c
index a5d1470832f51..72f23cd699ee4 100644
--- a/src/render/SDL_render.c
+++ b/src/render/SDL_render.c
@@ -4565,6 +4565,16 @@ void *SDL_GetRenderMetalCommandEncoder(SDL_Renderer *renderer)
     return NULL;
 }
 
+int SDL_AddVulkanRenderSemaphores(SDL_Renderer *renderer, Uint32 wait_stage_mask, Sint64 wait_semaphore, Sint64 signal_semaphore)
+{
+    CHECK_RENDERER_MAGIC(renderer, -1);
+
+    if (!renderer->AddVulkanRenderSemaphores) {
+        return SDL_Unsupported();
+    }
+    return renderer->AddVulkanRenderSemaphores(renderer, wait_stage_mask, wait_semaphore, signal_semaphore);
+}
+
 static SDL_BlendMode SDL_GetShortBlendMode(SDL_BlendMode blendMode)
 {
     if (blendMode == SDL_BLENDMODE_NONE_FULL) {
diff --git a/src/render/SDL_sysrender.h b/src/render/SDL_sysrender.h
index 9ab2a7979c4c2..99889f3f88170 100644
--- a/src/render/SDL_sysrender.h
+++ b/src/render/SDL_sysrender.h
@@ -216,6 +216,8 @@ struct SDL_Renderer
     void *(*GetMetalLayer)(SDL_Renderer *renderer);
     void *(*GetMetalCommandEncoder)(SDL_Renderer *renderer);
 
+    int (*AddVulkanRenderSemaphores)(SDL_Renderer *renderer, Uint32 wait_stage_mask, Sint64 wait_semaphore, Sint64 signal_semaphore);
+
     /* The current renderer info */
     SDL_RendererInfo info;
 
diff --git a/src/render/vulkan/SDL_render_vulkan.c b/src/render/vulkan/SDL_render_vulkan.c
index 7c3b0fd7a54ca..b80d130dfebd3 100644
--- a/src/render/vulkan/SDL_render_vulkan.c
+++ b/src/render/vulkan/SDL_render_vulkan.c
@@ -343,6 +343,14 @@ typedef struct
     VkSemaphore *renderingFinishedSemaphores;
     uint32_t currentSwapchainImageIndex;
 
+    VkPipelineStageFlags *waitDestStageMasks;
+    VkSemaphore *waitRenderSemaphores;
+    uint32_t waitRenderSemaphoreCount;
+    uint32_t waitRenderSemaphoreMax;
+    VkSemaphore *signalRenderSemaphores;
+    uint32_t signalRenderSemaphoreCount;
+    uint32_t signalRenderSemaphoreMax;
+
     /* Cached renderer properties */
     VULKAN_TextureData *textureRenderTarget;
     SDL_bool cliprectDirty;
@@ -454,6 +462,18 @@ static void VULKAN_DestroyAll(SDL_Renderer *renderer)
         return;
     }
 
+    if (rendererData->waitDestStageMasks) {
+        SDL_free(rendererData->waitDestStageMasks);
+        rendererData->waitDestStageMasks = NULL;
+    }
+    if (rendererData->waitRenderSemaphores) {
+        SDL_free(rendererData->waitRenderSemaphores);
+        rendererData->waitRenderSemaphores = NULL;
+    }
+    if (rendererData->signalRenderSemaphores) {
+        SDL_free(rendererData->signalRenderSemaphores);
+        rendererData->signalRenderSemaphores = NULL;
+    }
     if (rendererData->surfaceFormats != NULL) {
         SDL_free(rendererData->surfaceFormats);
         rendererData->surfaceFormats = NULL;
@@ -1009,6 +1029,12 @@ static VkResult VULKAN_IssueBatch(VULKAN_RenderData *rendererData)
     submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
     submitInfo.commandBufferCount = 1;
     submitInfo.pCommandBuffers = &rendererData->currentCommandBuffer;
+    if (rendererData->waitRenderSemaphoreCount > 0) {
+        submitInfo.waitSemaphoreCount = rendererData->waitRenderSemaphoreCount;
+        submitInfo.pWaitSemaphores = rendererData->waitRenderSemaphores;
+        submitInfo.pWaitDstStageMask = rendererData->waitDestStageMasks;
+        rendererData->waitRenderSemaphoreCount = 0;
+    }
     result = vkQueueSubmit(rendererData->graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE);
 
     VULKAN_WaitForGPU(rendererData);
@@ -2351,6 +2377,9 @@ static VkResult VULKAN_CreateSwapChain(SDL_Renderer *renderer, int w, int h)
 
     VULKAN_AcquireNextSwapchainImage(renderer);
 
+    SDL_PropertiesID props = SDL_GetRendererProperties(renderer);
+    SDL_SetNumberProperty(props, SDL_PROP_RENDERER_VULKAN_SWAPCHAIN_IMAGE_COUNT_NUMBER, rendererData->swapchainImageCount);
+
     return result;
 }
 
@@ -3833,6 +3862,48 @@ static SDL_Surface* VULKAN_RenderReadPixels(SDL_Renderer *renderer, const SDL_Re
     return output;
 }
 
+static int VULKAN_AddVulkanRenderSemaphores(SDL_Renderer *renderer, Uint32 wait_stage_mask, Sint64 wait_semaphore, Sint64 signal_semaphore)
+{
+    VULKAN_RenderData *rendererData = (VULKAN_RenderData *)renderer->driverdata;
+
+    if (wait_semaphore) {
+        if (rendererData->waitRenderSemaphoreCount == rendererData->waitRenderSemaphoreMax) {
+            /* Allocate an additional one at the end for the normal present wait */
+            VkPipelineStageFlags *waitDestStageMasks = (VkPipelineStageFlags *)SDL_realloc(rendererData->waitDestStageMasks, (rendererData->waitRenderSemaphoreMax + 2) * sizeof(*waitDestStageMasks));
+            if (!waitDestStageMasks) {
+                return -1;
+            }
+            rendererData->waitDestStageMasks = waitDestStageMasks;
+
+            VkSemaphore *semaphores = (VkSemaphore *)SDL_realloc(rendererData->waitRenderSemaphores, (rendererData->waitRenderSemaphoreMax + 2) * sizeof(*semaphores));
+            if (!semaphores) {
+                return -1;
+            }
+            rendererData->waitRenderSemaphores = semaphores;
+            ++rendererData->waitRenderSemaphoreMax;
+        }
+        rendererData->waitDestStageMasks[rendererData->waitRenderSemaphoreCount] = wait_stage_mask;
+        rendererData->waitRenderSemaphores[rendererData->waitRenderSemaphoreCount] = (VkSemaphore)wait_semaphore;
+        ++rendererData->waitRenderSemaphoreCount;
+    }
+
+    if (signal_semaphore) {
+        if (rendererData->signalRenderSemaphoreCount == rendererData->signalRenderSemaphoreMax) {
+            /* Allocate an additional one at the end for the normal present signal */
+            VkSemaphore *semaphores = (VkSemaphore *)SDL_realloc(rendererData->signalRenderSemaphores, (rendererData->signalRenderSemaphoreMax + 2) * sizeof(*semaphores));
+            if (!semaphores) {
+                return -1;
+            }
+            rendererData->signalRenderSemaphores = semaphores;
+            ++rendererData->signalRenderSemaphoreMax;
+        }
+        rendererData->signalRenderSemaphores[rendererData->signalRenderSemaphoreCount] = (VkSemaphore)signal_semaphore;
+        ++rendererData->signalRenderSemaphoreCount;
+    }
+
+    return 0;
+}
+
 static int VULKAN_RenderPresent(SDL_Renderer *renderer)
 {
     VULKAN_RenderData *rendererData = (VULKAN_RenderData *)renderer->driverdata;
@@ -3863,13 +3934,29 @@ static int VULKAN_RenderPresent(SDL_Renderer *renderer)
         VkPipelineStageFlags waitDestStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
         VkSubmitInfo submitInfo = { 0 };
         submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
-        submitInfo.waitSemaphoreCount = 1;
-        submitInfo.pWaitSemaphores = &rendererData->imageAvailableSemaphores[rendererData->currentCommandBufferIndex];
-        submitInfo.pWaitDstStageMask = &waitDestStageMask;
+        if (rendererData->waitRenderSemaphoreCount > 0) {
+            submitInfo.waitSemaphoreCount = rendererData->waitRenderSemaphoreCount + 1;
+            rendererData->waitRenderSemaphores[rendererData->waitRenderSemaphoreCount] = rendererData->imageAvailableSemaphores[rendererData->currentCommandBufferIndex];
+            rendererData->waitDestStageMasks[rendererData->waitRenderSemaphoreCount] = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
+            submitInfo.pWaitSemaphores = rendererData->waitRenderSemaphores;
+            submitInfo.pWaitDstStageMask = rendererData->waitDestStageMasks;
+            rendererData->waitRenderSemaphoreCount = 0;
+        } else {
+            submitInfo.waitSemaphoreCount = 1;
+            submitInfo.pWaitSemaphores = &rendererData->imageAvailableSemaphores[rendererData->currentCommandBufferIndex];
+            submitInfo.pWaitDstStageMask = &waitDestStageMask;
+        }
         submitInfo.commandBufferCount = 1;
         submitInfo.pCommandBuffers = &rendererData->currentCommandBuffer;
-        submitInfo.signalSemaphoreCount = 1;
-        submitInfo.pSignalSemaphores = &rendererData->renderingFinishedSemaphores[rendererData->currentCommandBufferIndex];
+        if (rendererData->signalRenderSemaphoreCount > 0) {
+            submitInfo.signalSemaphoreCount = rendererData->signalRenderSemaphoreCount + 1;
+            rendererData->signalRenderSemaphores[rendererData->signalRenderSemaphoreCount] = rendererData->renderingFinishedSemaphores[rendererData->currentCommandBufferIndex];
+            submitInfo.pSignalSemaphores = rendererData->signalRenderSemaphores;
+            rendererData->signalRenderSemaphoreCount = 0;
+        } else {
+            submitInfo.signalSemaphoreCount = 1;
+            submitInfo.pSignalSemaphores = &rendererData->renderingFinishedSemaphores[rendererData->currentCommandBufferIndex];
+        }
         result = vkQueueSubmit(rendererData->graphicsQueue, 1, &submitInfo, rendererData->fences[rendererData->currentCommandBufferIndex]);
         if (result != VK_SUCCESS) {
             SDL_LogError(SDL_LOG_CATEGORY_RENDER, "vkQueueSubmit(): %s\n", SDL_Vulkan_GetResultString(result));
@@ -3973,6 +4060,7 @@ SDL_Renderer *VULKAN_CreateRenderer(SDL_Window *window, SDL_PropertiesID create_
     renderer->InvalidateCachedState = VULKAN_InvalidateCachedState;
     renderer->RunCommandQueue = VULKAN_RunCommandQueue;
     renderer->RenderReadPixels = VULKAN_RenderReadPixels;
+    renderer->AddVulkanRenderSemaphores = VULKAN_AddVulkanRenderSemaphores;
     renderer->RenderPresent = VULKAN_RenderPresent;
     renderer->DestroyTexture = VULKAN_DestroyTexture;
     renderer->DestroyRenderer = VULKAN_DestroyRenderer;