SDL: GPU: Revise swapchain acquisition (#11633)

From 3ee39f6c3f2c20d49097fd4716d29972040c5de9 Mon Sep 17 00:00:00 2001
From: Evan Hemsley <[EMAIL REDACTED]>
Date: Wed, 11 Dec 2024 11:16:35 -0800
Subject: [PATCH] GPU: Revise swapchain acquisition (#11633)

---------

Co-authored-by: Lucas Murray <22484+lmurray@users.noreply.github.com>
---
 include/SDL3/SDL_gpu.h            | 100 ++++++++++++++++++++++--------
 src/dynapi/SDL_dynapi.sym         |   2 +
 src/dynapi/SDL_dynapi_overrides.h |   2 +
 src/dynapi/SDL_dynapi_procs.h     |   2 +
 src/gpu/SDL_gpu.c                 |  62 ++++++++++++++++--
 src/gpu/SDL_sysgpu.h              |  13 ++++
 src/gpu/d3d12/SDL_gpu_d3d12.c     |  67 ++++++++++++++++++--
 src/gpu/metal/SDL_gpu_metal.m     |  71 ++++++++++++++++++---
 src/gpu/vulkan/SDL_gpu_vulkan.c   |  69 ++++++++++++++++++---
 9 files changed, 337 insertions(+), 51 deletions(-)

diff --git a/include/SDL3/SDL_gpu.h b/include/SDL3/SDL_gpu.h
index 8b7417f16be62..40f11a5fc227f 100644
--- a/include/SDL3/SDL_gpu.h
+++ b/include/SDL3/SDL_gpu.h
@@ -1068,8 +1068,7 @@ typedef enum SDL_GPUSamplerAddressMode
  * Specifies the timing that will be used to present swapchain textures to the
  * OS.
  *
- * Note that this value affects the behavior of
- * SDL_AcquireGPUSwapchainTexture. VSYNC mode will always be supported.
+ * VSYNC mode will always be supported.
  * IMMEDIATE and MAILBOX modes may not be supported on certain systems.
  *
  * It is recommended to query SDL_WindowSupportsGPUPresentMode after claiming
@@ -1077,17 +1076,12 @@ typedef enum SDL_GPUSamplerAddressMode
  *
  * - VSYNC: Waits for vblank before presenting. No tearing is possible. If
  *   there is a pending image to present, the new image is enqueued for
- *   presentation. Disallows tearing at the cost of visual latency. When using
- *   this present mode, AcquireGPUSwapchainTexture will block if too many
- *   frames are in flight.
+ *   presentation. Disallows tearing at the cost of visual latency.
  * - IMMEDIATE: Immediately presents. Lowest latency option, but tearing may
- *   occur. When using this mode, AcquireGPUSwapchainTexture will fill the
- *   swapchain texture pointer with NULL if too many frames are in flight.
+ *   occur.
  * - MAILBOX: Waits for vblank before presenting. No tearing is possible. If
  *   there is a pending image to present, the pending image is replaced by the
- *   new image. Similar to VSYNC, but with reduced visual latency. When using
- *   this mode, AcquireGPUSwapchainTexture will fill the swapchain texture
- *   pointer with NULL if too many frames are in flight.
+ *   new image. Similar to VSYNC, but with reduced visual latency.
  *
  * \since This enum is available since SDL 3.1.3
  *
@@ -3442,6 +3436,8 @@ extern SDL_DECLSPEC bool SDLCALL SDL_WindowSupportsGPUPresentMode(
  * \returns true on success, or false on failure; call SDL_GetError() for more
  *          information.
  *
+ * \threadsafety This function should only be called from the thread that created the window.
+ *
  * \since This function is available since SDL 3.1.3.
  *
  * \sa SDL_AcquireGPUSwapchainTexture
@@ -3501,8 +3497,8 @@ extern SDL_DECLSPEC bool SDLCALL SDL_SetGPUSwapchainParameters(
  *
  * The default value when the device is created is 2. This means that after
  * you have submitted 2 frames for presentation, if the GPU has not finished
- * working on the first frame, SDL_AcquireGPUSwapchainTexture() will block or
- * return false depending on the present mode.
+ * working on the first frame, SDL_AcquireGPUSwapchainTexture() will fill the swapchain texture pointer with NULL,
+ * and SDL_WaitAndAcquireGPUSwapchainTexture() will block.
  *
  * Higher values increase throughput at the expense of visual latency. Lower
  * values decrease visual latency at the expense of throughput.
@@ -3514,9 +3510,7 @@ extern SDL_DECLSPEC bool SDLCALL SDL_SetGPUSwapchainParameters(
  *
  * \param device a GPU context.
  * \param allowed_frames_in_flight the maximum number of frames that can be
- *                                 pending on the GPU before
- *                                 AcquireSwapchainTexture blocks or returns
- *                                 false.
+ *                                 pending on the GPU.
  * \returns true if successful, false on error; call SDL_GetError() for more
  *          information.
  *
@@ -3547,20 +3541,17 @@ extern SDL_DECLSPEC SDL_GPUTextureFormat SDLCALL SDL_GetGPUSwapchainTextureForma
  * When a swapchain texture is acquired on a command buffer, it will
  * automatically be submitted for presentation when the command buffer is
  * submitted. The swapchain texture should only be referenced by the command
- * buffer used to acquire it. The swapchain texture handle can be filled in
- * with NULL under certain conditions. This is not necessarily an error. If
- * this function returns false then there is an error.
+ * buffer used to acquire it.
+ *
+ * This function will fill the swapchain texture handle with NULL if too many frames are in flight.
+ * This is not an error.
+ * The best practice is to call SDL_CancelGPUCommandBuffer if the swapchain texture
+ * handle is NULL to avoid enqueuing needless work on the GPU.
  *
  * The swapchain texture is managed by the implementation and must not be
  * freed by the user. You MUST NOT call this function from any thread other
  * than the one that created the window.
  *
- * When using SDL_GPU_PRESENTMODE_VSYNC, this function will block if too many
- * frames are in flight. Otherwise, this function will fill the swapchain
- * texture handle with NULL if too many frames are in flight. The best
- * practice is to call SDL_CancelGPUCommandBuffer if the swapchain texture
- * handle is NULL to avoid enqueuing needless work on the GPU.
- *
  * \param command_buffer a command buffer.
  * \param window a window that has been claimed.
  * \param swapchain_texture a pointer filled in with a swapchain texture
@@ -3572,14 +3563,17 @@ extern SDL_DECLSPEC SDL_GPUTextureFormat SDLCALL SDL_GetGPUSwapchainTextureForma
  * \returns true on success, false on error; call SDL_GetError() for more
  *          information.
  *
+ * \threadsafety This function should only be called from the thread that created the window.
+ *
  * \since This function is available since SDL 3.1.3.
  *
- * \sa SDL_GPUPresentMode
  * \sa SDL_ClaimWindowForGPUDevice
  * \sa SDL_SubmitGPUCommandBuffer
  * \sa SDL_SubmitGPUCommandBufferAndAcquireFence
  * \sa SDL_CancelGPUCommandBuffer
  * \sa SDL_GetWindowSizeInPixels
+ * \sa SDL_WaitForGPUSwapchain
+ * \sa SDL_SetGPUAllowedFramesInFlight
  */
 extern SDL_DECLSPEC bool SDLCALL SDL_AcquireGPUSwapchainTexture(
     SDL_GPUCommandBuffer *command_buffer,
@@ -3588,6 +3582,62 @@ extern SDL_DECLSPEC bool SDLCALL SDL_AcquireGPUSwapchainTexture(
     Uint32 *swapchain_texture_width,
     Uint32 *swapchain_texture_height);
 
+/**
+ * Blocks the thread until a swapchain texture is available to be acquired.
+ *
+ * \param device a GPU context.
+ * \param window a window that has been claimed.
+ * \returns true on success, false on failure; call SDL_GetError() for more
+ *          information.
+ *
+ * \threadsafety This function should only be called from the thread that created the window.
+ *
+ * \since This function is available since SDL 3.2.0.
+ *
+ * \sa SDL_AcquireGPUSwapchainTexture
+ * \sa SDL_SetGPUAllowedFramesInFlight
+ */
+extern SDL_DECLSPEC bool SDLCALL SDL_WaitForGPUSwapchain(
+    SDL_GPUDevice *device,
+    SDL_Window *window);
+
+/**
+ * Blocks the thread until a swapchain texture is available to be acquired, and then acquires it.
+ *
+ * When a swapchain texture is acquired on a command buffer, it will
+ * automatically be submitted for presentation when the command buffer is
+ * submitted. The swapchain texture should only be referenced by the command
+ * buffer used to acquire it. It is an error to call SDL_CancelGPUCommandBuffer() after a swapchain texture is acquired.
+ *
+ * The swapchain texture is managed by the implementation and must not be
+ * freed by the user. You MUST NOT call this function from any thread other
+ * than the one that created the window.
+ *
+ * \param command_buffer a command buffer.
+ * \param window a window that has been claimed.
+ * \param swapchain_texture a pointer filled in with a swapchain texture
+ *                          handle.
+ * \param swapchain_texture_width a pointer filled in with the swapchain
+ *                                texture width, may be NULL.
+ * \param swapchain_texture_height a pointer filled in with the swapchain
+ *                                 texture height, may be NULL.
+ * \returns true on success, false on error; call SDL_GetError() for more
+ *          information.
+ *
+ * \threadsafety This function should only be called from the thread that created the window.
+ *
+ * \since This function is available since SDL 3.2.0.
+ *
+ * \sa SDL_SubmitGPUCommandBuffer
+ * \sa SDL_SubmitGPUCommandBufferAndAcquireFence
+ */
+extern SDL_DECLSPEC bool SDLCALL SDL_WaitAndAcquireGPUSwapchainTexture(
+    SDL_GPUCommandBuffer *command_buffer,
+    SDL_Window *window,
+    SDL_GPUTexture **swapchain_texture,
+    Uint32 *swapchain_texture_width,
+    Uint32 *swapchain_texture_height);
+
 /**
  * Submits a command buffer so its commands can be processed on the GPU.
  *
diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym
index cba2c637298b0..d575bcef93794 100644
--- a/src/dynapi/SDL_dynapi.sym
+++ b/src/dynapi/SDL_dynapi.sym
@@ -974,6 +974,7 @@ SDL3_0.0.0 {
     SDL_WaitEventTimeout;
     SDL_WaitForGPUFences;
     SDL_WaitForGPUIdle;
+    SDL_WaitForGPUSwapchain;
     SDL_WaitProcess;
     SDL_WaitSemaphore;
     SDL_WaitSemaphoreTimeout;
@@ -1205,6 +1206,7 @@ SDL3_0.0.0 {
     SDL_RunOnMainThread;
     SDL_SetGPUAllowedFramesInFlight;
     SDL_RenderTextureAffine;
+    SDL_WaitAndAcquireGPUSwapchainTexture;
     # 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 7dd37385ad352..608d59ca1ecb1 100644
--- a/src/dynapi/SDL_dynapi_overrides.h
+++ b/src/dynapi/SDL_dynapi_overrides.h
@@ -999,6 +999,7 @@
 #define SDL_WaitEventTimeout SDL_WaitEventTimeout_REAL
 #define SDL_WaitForGPUFences SDL_WaitForGPUFences_REAL
 #define SDL_WaitForGPUIdle SDL_WaitForGPUIdle_REAL
+#define SDL_WaitForGPUSwapchain SDL_WaitForGPUSwapchain_REAL
 #define SDL_WaitProcess SDL_WaitProcess_REAL
 #define SDL_WaitSemaphore SDL_WaitSemaphore_REAL
 #define SDL_WaitSemaphoreTimeout SDL_WaitSemaphoreTimeout_REAL
@@ -1230,3 +1231,4 @@
 #define SDL_RunOnMainThread SDL_RunOnMainThread_REAL
 #define SDL_SetGPUAllowedFramesInFlight SDL_SetGPUAllowedFramesInFlight_REAL
 #define SDL_RenderTextureAffine SDL_RenderTextureAffine_REAL
+#define SDL_WaitAndAcquireGPUSwapchainTexture SDL_WaitAndAcquireGPUSwapchainTexture_REAL
diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h
index 1a9acd50010f4..4611595282db8 100644
--- a/src/dynapi/SDL_dynapi_procs.h
+++ b/src/dynapi/SDL_dynapi_procs.h
@@ -1009,6 +1009,7 @@ SDL_DYNAPI_PROC(bool,SDL_WaitEvent,(SDL_Event *a),(a),return)
 SDL_DYNAPI_PROC(bool,SDL_WaitEventTimeout,(SDL_Event *a, Sint32 b),(a,b),return)
 SDL_DYNAPI_PROC(bool,SDL_WaitForGPUFences,(SDL_GPUDevice *a, bool b, SDL_GPUFence *const *c, Uint32 d),(a,b,c,d),return)
 SDL_DYNAPI_PROC(bool,SDL_WaitForGPUIdle,(SDL_GPUDevice *a),(a),return)
+SDL_DYNAPI_PROC(bool,SDL_WaitForGPUSwapchain,(SDL_GPUDevice *a, SDL_Window *b),(a,b),return)
 SDL_DYNAPI_PROC(bool,SDL_WaitProcess,(SDL_Process *a, bool b, int *c),(a,b,c),return)
 SDL_DYNAPI_PROC(void,SDL_WaitSemaphore,(SDL_Semaphore *a),(a),)
 SDL_DYNAPI_PROC(bool,SDL_WaitSemaphoreTimeout,(SDL_Semaphore *a, Sint32 b),(a,b),return)
@@ -1236,3 +1237,4 @@ SDL_DYNAPI_PROC(bool,SDL_IsMainThread,(void),(),return)
 SDL_DYNAPI_PROC(bool,SDL_RunOnMainThread,(SDL_MainThreadCallback a,void *b,bool c),(a,b,c),return)
 SDL_DYNAPI_PROC(bool,SDL_SetGPUAllowedFramesInFlight,(SDL_GPUDevice *a,Uint32 b),(a,b),return)
 SDL_DYNAPI_PROC(bool,SDL_RenderTextureAffine,(SDL_Renderer *a,SDL_Texture *b,const SDL_FRect *c,const SDL_FPoint *d,const SDL_FPoint *e,const SDL_FPoint *f),(a,b,c,d,e,f),return)
+SDL_DYNAPI_PROC(bool,SDL_WaitAndAcquireGPUSwapchainTexture,(SDL_GPUCommandBuffer *a,SDL_Window *b,SDL_GPUTexture **c,Uint32 *d,Uint32 *e),(a,b,c,d,e),return)
diff --git a/src/gpu/SDL_gpu.c b/src/gpu/SDL_gpu.c
index 5763e793dd729..bc1cb1a288182 100644
--- a/src/gpu/SDL_gpu.c
+++ b/src/gpu/SDL_gpu.c
@@ -2694,16 +2694,13 @@ bool SDL_AcquireGPUSwapchainTexture(
     CommandBufferCommonHeader *commandBufferHeader = (CommandBufferCommonHeader *)command_buffer;
 
     if (command_buffer == NULL) {
-        SDL_InvalidParamError("command_buffer");
-        return false;
+        return SDL_InvalidParamError("command_buffer");
     }
     if (window == NULL) {
-        SDL_InvalidParamError("window");
-        return false;
+        return SDL_InvalidParamError("window");
     }
     if (swapchain_texture == NULL) {
-        SDL_InvalidParamError("swapchain_texture");
-        return false;
+        return SDL_InvalidParamError("swapchain_texture");
     }
 
     if (COMMAND_BUFFER_DEVICE->debug_mode) {
@@ -2725,6 +2722,59 @@ bool SDL_AcquireGPUSwapchainTexture(
     return result;
 }
 
+bool SDL_WaitForGPUSwapchain(
+    SDL_GPUDevice *device,
+    SDL_Window *window)
+{
+    CHECK_DEVICE_MAGIC(device, false);
+
+    if (window == NULL) {
+        return SDL_InvalidParamError("window");
+    }
+
+    return device->WaitForSwapchain(
+        device->driverData,
+        window);
+}
+
+bool SDL_WaitAndAcquireGPUSwapchainTexture(
+    SDL_GPUCommandBuffer *command_buffer,
+    SDL_Window *window,
+    SDL_GPUTexture **swapchain_texture,
+    Uint32 *swapchain_texture_width,
+    Uint32 *swapchain_texture_height)
+{
+    CommandBufferCommonHeader *commandBufferHeader = (CommandBufferCommonHeader *)command_buffer;
+
+    if (command_buffer == NULL) {
+        return SDL_InvalidParamError("command_buffer");
+    }
+    if (window == NULL) {
+        return SDL_InvalidParamError("window");
+    }
+    if (swapchain_texture == NULL) {
+        return SDL_InvalidParamError("swapchain_texture");
+    }
+
+    if (COMMAND_BUFFER_DEVICE->debug_mode) {
+        CHECK_COMMAND_BUFFER_RETURN_FALSE
+        CHECK_ANY_PASS_IN_PROGRESS("Cannot acquire a swapchain texture during a pass!", false)
+    }
+
+    bool result = COMMAND_BUFFER_DEVICE->WaitAndAcquireSwapchainTexture(
+        command_buffer,
+        window,
+        swapchain_texture,
+        swapchain_texture_width,
+        swapchain_texture_height);
+
+    if (*swapchain_texture != NULL){
+        commandBufferHeader->swapchain_texture_acquired = true;
+    }
+
+    return result;
+}
+
 bool SDL_SubmitGPUCommandBuffer(
     SDL_GPUCommandBuffer *command_buffer)
 {
diff --git a/src/gpu/SDL_sysgpu.h b/src/gpu/SDL_sysgpu.h
index 2cf8f6459b74a..901fd9aba2aaa 100644
--- a/src/gpu/SDL_sysgpu.h
+++ b/src/gpu/SDL_sysgpu.h
@@ -811,6 +811,17 @@ struct SDL_GPUDevice
         Uint32 *swapchainTextureWidth,
         Uint32 *swapchainTextureHeight);
 
+    bool (*WaitForSwapchain)(
+        SDL_GPURenderer *driverData,
+        SDL_Window *window);
+
+    bool (*WaitAndAcquireSwapchainTexture)(
+        SDL_GPUCommandBuffer *commandBuffer,
+        SDL_Window *window,
+        SDL_GPUTexture **swapchainTexture,
+        Uint32 *swapchainTextureWidth,
+        Uint32 *swapchainTextureHeight);
+
     bool (*Submit)(
         SDL_GPUCommandBuffer *commandBuffer);
 
@@ -937,6 +948,8 @@ struct SDL_GPUDevice
     ASSIGN_DRIVER_FUNC(GetSwapchainTextureFormat, name)     \
     ASSIGN_DRIVER_FUNC(AcquireCommandBuffer, name)          \
     ASSIGN_DRIVER_FUNC(AcquireSwapchainTexture, name)       \
+    ASSIGN_DRIVER_FUNC(WaitForSwapchain, name)              \
+    ASSIGN_DRIVER_FUNC(WaitAndAcquireSwapchainTexture, name)\
     ASSIGN_DRIVER_FUNC(Submit, name)                        \
     ASSIGN_DRIVER_FUNC(SubmitAndAcquireFence, name)         \
     ASSIGN_DRIVER_FUNC(Cancel, name)                        \
diff --git a/src/gpu/d3d12/SDL_gpu_d3d12.c b/src/gpu/d3d12/SDL_gpu_d3d12.c
index 7b024f0b024b6..f42c72bf80169 100644
--- a/src/gpu/d3d12/SDL_gpu_d3d12.c
+++ b/src/gpu/d3d12/SDL_gpu_d3d12.c
@@ -7124,7 +7124,32 @@ static SDL_GPUCommandBuffer *D3D12_AcquireCommandBuffer(
     return (SDL_GPUCommandBuffer *)commandBuffer;
 }
 
-static bool D3D12_AcquireSwapchainTexture(
+static bool D3D12_WaitForSwapchain(
+    SDL_GPURenderer *driverData,
+    SDL_Window *window)
+{
+    D3D12Renderer *renderer = (D3D12Renderer *)driverData;
+    D3D12WindowData *windowData = D3D12_INTERNAL_FetchWindowData(window);
+
+    if (windowData == NULL) {
+        SET_STRING_ERROR_AND_RETURN("Cannot wait for a swapchain from an unclaimed window!", false);
+    }
+
+    if (windowData->inFlightFences[windowData->frameCounter] != NULL) {
+        if (!D3D12_WaitForFences(
+            driverData,
+            true,
+            &windowData->inFlightFences[windowData->frameCounter],
+            1)) {
+            return false;
+        }
+    }
+
+    return true;
+}
+
+static bool D3D12_INTERNAL_AcquireSwapchainTexture(
+    bool block,
     SDL_GPUCommandBuffer *commandBuffer,
     SDL_Window *window,
     SDL_GPUTexture **swapchainTexture,
@@ -7164,7 +7189,7 @@ static bool D3D12_AcquireSwapchainTexture(
     }
 
     if (windowData->inFlightFences[windowData->frameCounter] != NULL) {
-        if (windowData->present_mode == SDL_GPU_PRESENTMODE_VSYNC) {
+        if (block) {
             // In VSYNC mode, block until the least recent presented frame is done
             if (!D3D12_WaitForFences(
                 (SDL_GPURenderer *)renderer,
@@ -7174,13 +7199,11 @@ static bool D3D12_AcquireSwapchainTexture(
                 return false;
             }
         } else {
+            // If we are not blocking and the least recent fence is not signaled,
+            // return true to indicate that there is no error but rendering should be skipped.
             if (!D3D12_QueryFence(
                     (SDL_GPURenderer *)renderer,
                     windowData->inFlightFences[windowData->frameCounter])) {
-                /*
-                 * In MAILBOX or IMMEDIATE mode, if the least recent fence is not signaled,
-                 * return true to indicate that there is no error but rendering should be skipped
-                 */
                 return true;
             }
         }
@@ -7238,6 +7261,38 @@ static bool D3D12_AcquireSwapchainTexture(
     return true;
 }
 
+static bool D3D12_AcquireSwapchainTexture(
+    SDL_GPUCommandBuffer *command_buffer,
+    SDL_Window *window,
+    SDL_GPUTexture **swapchain_texture,
+    Uint32 *swapchain_texture_width,
+    Uint32 *swapchain_texture_height
+) {
+    return D3D12_INTERNAL_AcquireSwapchainTexture(
+        false,
+        command_buffer,
+        window,
+        swapchain_texture,
+        swapchain_texture_width,
+        swapchain_texture_height);
+}
+
+static bool D3D12_WaitAndAcquireSwapchainTexture(
+    SDL_GPUCommandBuffer *command_buffer,
+    SDL_Window *window,
+    SDL_GPUTexture **swapchain_texture,
+    Uint32 *swapchain_texture_width,
+    Uint32 *swapchain_texture_height
+) {
+    return D3D12_INTERNAL_AcquireSwapchainTexture(
+        true,
+        command_buffer,
+        window,
+        swapchain_texture,
+        swapchain_texture_width,
+        swapchain_texture_height);
+}
+
 static void D3D12_INTERNAL_PerformPendingDestroys(D3D12Renderer *renderer)
 {
     SDL_LockMutex(renderer->disposeLock);
diff --git a/src/gpu/metal/SDL_gpu_metal.m b/src/gpu/metal/SDL_gpu_metal.m
index 981355a3ad88b..be64b366ff14c 100644
--- a/src/gpu/metal/SDL_gpu_metal.m
+++ b/src/gpu/metal/SDL_gpu_metal.m
@@ -3671,7 +3671,34 @@ static void METAL_ReleaseWindow(
     }
 }
 
-static bool METAL_AcquireSwapchainTexture(
+static bool METAL_WaitForSwapchain(
+    SDL_GPURenderer *driverData,
+    SDL_Window *window)
+{
+    @autoreleasepool {
+        MetalRenderer *renderer = (MetalRenderer *)driverData;
+        MetalWindowData *windowData = METAL_INTERNAL_FetchWindowData(window);
+
+        if (windowData == NULL) {
+            SET_STRING_ERROR_AND_RETURN("Cannot wait for a swapchain from an unclaimed window!", false);
+        }
+
+        if (windowData->inFlightFences[windowData->frameCounter] != NULL) {
+            if (!METAL_WaitForFences(
+                driverData,
+                true,
+                &windowData->inFlightFences[windowData->frameCounter],
+                1)) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+}
+
+static bool METAL_INTERNAL_AcquireSwapchainTexture(
+    bool block,
     SDL_GPUCommandBuffer *commandBuffer,
     SDL_Window *window,
     SDL_GPUTexture **texture,
@@ -3709,8 +3736,8 @@ static bool METAL_AcquireSwapchainTexture(
         }
 
         if (windowData->inFlightFences[windowData->frameCounter] != NULL) {
-            if (windowData->presentMode == SDL_GPU_PRESENTMODE_VSYNC) {
-                // In VSYNC mode, block until the least recent presented frame is done
+            if (block) {
+                // If we are blocking, just wait for the fence!
                 if (!METAL_WaitForFences(
                     (SDL_GPURenderer *)renderer,
                     true,
@@ -3719,13 +3746,11 @@ static bool METAL_AcquireSwapchainTexture(
                     return false;
                 }
             } else {
+                // If we are not blocking and the least recent fence is not signaled,
+                // return true to indicate that there is no error but rendering should be skipped.
                 if (!METAL_QueryFence(
                         (SDL_GPURenderer *)metalCommandBuffer->renderer,
                         windowData->inFlightFences[windowData->frameCounter])) {
-                    /*
-                    * In IMMEDIATE mode, if the least recent fence is not signaled,
-                    * return true to indicate that there is no error but rendering should be skipped
-                    */
                     return true;
                 }
             }
@@ -3757,6 +3782,38 @@ static bool METAL_AcquireSwapchainTexture(
     }
 }
 
+static bool METAL_AcquireSwapchainTexture(
+    SDL_GPUCommandBuffer *command_buffer,
+    SDL_Window *window,
+    SDL_GPUTexture **swapchain_texture,
+    Uint32 *swapchain_texture_width,
+    Uint32 *swapchain_texture_height
+) {
+    return METAL_INTERNAL_AcquireSwapchainTexture(
+        false,
+        command_buffer,
+        window,
+        swapchain_texture,
+        swapchain_texture_width,
+        swapchain_texture_height);
+}
+
+static bool METAL_WaitAndAcquireSwapchainTexture(
+    SDL_GPUCommandBuffer *command_buffer,
+    SDL_Window *window,
+    SDL_GPUTexture **swapchain_texture,
+    Uint32 *swapchain_texture_width,
+    Uint32 *swapchain_texture_height
+) {
+    return METAL_INTERNAL_AcquireSwapchainTexture(
+        true,
+        command_buffer,
+        window,
+        swapchain_texture,
+        swapchain_texture_width,
+        swapchain_texture_height);
+}
+
 static SDL_GPUTextureFormat METAL_GetSwapchainTextureFormat(
     SDL_GPURenderer *driverData,
     SDL_Window *window)
diff --git a/src/gpu/vulkan/SDL_gpu_vulkan.c b/src/gpu/vulkan/SDL_gpu_vulkan.c
index 359aecdfeb0f8..277e4246b454e 100644
--- a/src/gpu/vulkan/SDL_gpu_vulkan.c
+++ b/src/gpu/vulkan/SDL_gpu_vulkan.c
@@ -9655,7 +9655,32 @@ static Uint32 VULKAN_INTERNAL_RecreateSwapchain(
     return VULKAN_INTERNAL_CreateSwapchain(renderer, windowData);
 }
 
-static bool VULKAN_AcquireSwapchainTexture(
+static bool VULKAN_WaitForSwapchain(
+    SDL_GPURenderer *driverData,
+    SDL_Window *window)
+{
+    VulkanRenderer *renderer = (VulkanRenderer *)driverData;
+    WindowData *windowData = VULKAN_INTERNAL_FetchWindowData(window);
+
+    if (windowData == NULL) {
+        SET_STRING_ERROR_AND_RETURN("Cannot wait for a swapchain from an unclaimed window!", false);
+    }
+
+    if (windowData->inFlightFences[windowData->frameCounter] != NULL) {
+        if (!VULKAN_WaitForFences(
+            driverData,
+            true,
+            &windowData->inFlightFences[windowData->frameCounter],
+            1)) {
+            return false;
+        }
+    }
+
+    return true;
+}
+
+static bool VULKAN_INTERNAL_AcquireSwapchainTexture(
+    bool block,
     SDL_GPUCommandBuffer *commandBuffer,
     SDL_Window *window,
     SDL_GPUTexture **swapchainTexture,
@@ -9708,8 +9733,8 @@ static bool VULKAN_AcquireSwapchainTexture(
     }
 
     if (windowData->inFlightFences[windowData->frameCounter] != NULL) {
-        if (windowData->presentMode == SDL_GPU_PRESENTMODE_VSYNC) {
-            // In VSYNC mode, block until the least recent presented frame is done
+        if (block) {
+            // If we are blocking, just wait for the fence!
             if (!VULKAN_WaitForFences(
                 (SDL_GPURenderer *)renderer,
                 true,
@@ -9718,13 +9743,11 @@ static bool VULKAN_AcquireSwapchainTexture(
                 return false;
             }
         } else {
+            // If we are not blocking and the least recent fence is not signaled,
+            // return true to indicate that there is no error but rendering should be skipped.
             if (!VULKAN_QueryFence(
                     (SDL_GPURenderer *)renderer,
                     windowData->inFlightFences[windowData->frameCounter])) {
-                /*
-                 * In MAILBOX or IMMEDIATE mode, if the least recent fence is not signaled,
-                 * return true to indicate that there is no error but rendering should be skipped
-                 */
                 return true;
             }
         }
@@ -9843,6 +9866,38 @@ static bool VULKAN_AcquireSwapchainTexture(
     return true;
 }
 
+static bool VULKAN_AcquireSwapchainTexture(
+    SDL_GPUCommandBuffer *command_buffer,
+    SDL_Window *window,
+    SDL_GPUTexture **swapchain_texture,
+    Uint32 *swapchain_texture_width,
+    Uint32 *swapchain_texture_height
+) {
+    return VULKAN_INTERNAL_AcquireSwapchainTexture(
+        false,
+        command_buffer,
+        window,
+        swapchain_texture,
+        swapchain_texture_width,
+        swapchain_texture_height);
+}
+
+static bool VULKAN_WaitAndAcquireSwapchainTexture(
+    SDL_GPUCommandBuffer *command_buffer,
+    SDL_Window *window,
+    SDL_GPUTexture **swapchain_texture,
+    Uint32 *swapchain_texture_width,
+    Uint32 *swapchain_texture_height
+) {
+    return VULKAN_INTERNAL_AcquireSwapchainTexture(
+        true, 
+        command_buffer, 
+        window, 
+        swapchain_texture,
+        swapchain_texture_width,
+        swapchain_texture_height);
+}
+
 static SDL_GPUTextureFormat VULKAN_GetSwapchainTextureFormat(
     SDL_GPURenderer *driverData,
     SDL_Window *window)