SDL: GPU: Add SetGPUAllowedFramesInFlight (#11599)

From fa5f84fb6ec308bf7bd4819b6c6887a684571695 Mon Sep 17 00:00:00 2001
From: Evan Hemsley <[EMAIL REDACTED]>
Date: Fri, 6 Dec 2024 11:56:20 -0800
Subject: [PATCH] GPU: Add SetGPUAllowedFramesInFlight (#11599)

---
 include/SDL3/SDL_gpu.h            | 23 +++++++++++++++++++++++
 src/dynapi/SDL_dynapi.sym         |  1 +
 src/dynapi/SDL_dynapi_overrides.h |  1 +
 src/dynapi/SDL_dynapi_procs.h     |  1 +
 src/gpu/SDL_gpu.c                 | 19 +++++++++++++++++++
 src/gpu/SDL_sysgpu.h              |  5 +++++
 src/gpu/d3d12/SDL_gpu_d3d12.c     | 28 ++++++++++++++++++++++------
 src/gpu/metal/SDL_gpu_metal.m     | 20 +++++++++++++++++++-
 src/gpu/vulkan/SDL_gpu_vulkan.c   | 20 ++++++++++++++++++--
 9 files changed, 109 insertions(+), 9 deletions(-)

diff --git a/include/SDL3/SDL_gpu.h b/include/SDL3/SDL_gpu.h
index 0e7c9fd84a8cf..5cfb487bc5241 100644
--- a/include/SDL3/SDL_gpu.h
+++ b/include/SDL3/SDL_gpu.h
@@ -3496,6 +3496,29 @@ extern SDL_DECLSPEC bool SDLCALL SDL_SetGPUSwapchainParameters(
     SDL_GPUSwapchainComposition swapchain_composition,
     SDL_GPUPresentMode present_mode);
 
+/**
+ * Configures the maximum allowed number of frames in flight.
+ *
+ * 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.
+ *
+ * Higher values increase throughput at the expense of visual latency.
+ * Lower values decrease visual latency at the expense of throughput.
+ *
+ * Note that calling this function will stall and flush the command queue to prevent synchronization issues.
+ *
+ * The minimum value of allowed frames in flight is 1, and the maximum is 3.
+ *
+ * \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.
+ * \returns true if successful, false on error; call SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 3.2.0.
+ */
+extern SDL_DECLSPEC bool SDLCALL SDL_SetGPUAllowedFramesInFlight(
+    SDL_GPUDevice *device,
+    Uint32 allowed_frames_in_flight);
+
 /**
  * Obtains the texture format of the swapchain for the given window.
  *
diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym
index 1ca616f709e40..145a4622ee265 100644
--- a/src/dynapi/SDL_dynapi.sym
+++ b/src/dynapi/SDL_dynapi.sym
@@ -1203,6 +1203,7 @@ SDL3_0.0.0 {
     SDL_ShowFileDialogWithProperties;
     SDL_IsMainThread;
     SDL_RunOnMainThread;
+    SDL_SetGPUAllowedFramesInFlight;
     # 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 691e7f184c90a..652e2cd0c0833 100644
--- a/src/dynapi/SDL_dynapi_overrides.h
+++ b/src/dynapi/SDL_dynapi_overrides.h
@@ -1228,3 +1228,4 @@
 #define SDL_ShowFileDialogWithProperties SDL_ShowFileDialogWithProperties_REAL
 #define SDL_IsMainThread SDL_IsMainThread_REAL
 #define SDL_RunOnMainThread SDL_RunOnMainThread_REAL
+#define SDL_SetGPUAllowedFramesInFlight SDL_SetGPUAllowedFramesInFlight_REAL
diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h
index d39ad72331ab9..f36cc06f8a1bb 100644
--- a/src/dynapi/SDL_dynapi_procs.h
+++ b/src/dynapi/SDL_dynapi_procs.h
@@ -1234,3 +1234,4 @@ SDL_DYNAPI_PROC(bool,SDL_LoadFileAsync,(const char *a, SDL_AsyncIOQueue *b, void
 SDL_DYNAPI_PROC(void,SDL_ShowFileDialogWithProperties,(SDL_FileDialogType a, SDL_DialogFileCallback b, void *c, SDL_PropertiesID d),(a,b,c,d),)
 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)
diff --git a/src/gpu/SDL_gpu.c b/src/gpu/SDL_gpu.c
index 83ffbf6f9725e..8bd63c960ad84 100644
--- a/src/gpu/SDL_gpu.c
+++ b/src/gpu/SDL_gpu.c
@@ -2650,6 +2650,25 @@ bool SDL_SetGPUSwapchainParameters(
         present_mode);
 }
 
+bool SDL_SetGPUAllowedFramesInFlight(
+    SDL_GPUDevice *device,
+    Uint32 allowed_frames_in_flight)
+{
+    CHECK_DEVICE_MAGIC(device, false);
+
+    if (device->debug_mode) {
+        if (allowed_frames_in_flight < 1 || allowed_frames_in_flight > 3)
+        {
+            SDL_assert_release(!"allowed_frames_in_flight value must be between 1 and 3!");
+        }
+    }
+
+    allowed_frames_in_flight = SDL_clamp(allowed_frames_in_flight, 1, 3);
+    return device->SetAllowedFramesInFlight(
+        device->driverData,
+        allowed_frames_in_flight);
+}
+
 SDL_GPUTextureFormat SDL_GetGPUSwapchainTextureFormat(
     SDL_GPUDevice *device,
     SDL_Window *window)
diff --git a/src/gpu/SDL_sysgpu.h b/src/gpu/SDL_sysgpu.h
index bd2a2f42bc7f2..8806d2fb8b78b 100644
--- a/src/gpu/SDL_sysgpu.h
+++ b/src/gpu/SDL_sysgpu.h
@@ -791,6 +791,10 @@ struct SDL_GPUDevice
         SDL_GPUSwapchainComposition swapchainComposition,
         SDL_GPUPresentMode presentMode);
 
+    bool (*SetAllowedFramesInFlight)(
+        SDL_GPURenderer *driverData,
+        Uint32 allowedFramesInFlight);
+
     SDL_GPUTextureFormat (*GetSwapchainTextureFormat)(
         SDL_GPURenderer *driverData,
         SDL_Window *window);
@@ -927,6 +931,7 @@ struct SDL_GPUDevice
     ASSIGN_DRIVER_FUNC(ClaimWindow, name)                   \
     ASSIGN_DRIVER_FUNC(ReleaseWindow, name)                 \
     ASSIGN_DRIVER_FUNC(SetSwapchainParameters, name)        \
+    ASSIGN_DRIVER_FUNC(SetAllowedFramesInFlight, name)      \
     ASSIGN_DRIVER_FUNC(GetSwapchainTextureFormat, name)     \
     ASSIGN_DRIVER_FUNC(AcquireCommandBuffer, name)          \
     ASSIGN_DRIVER_FUNC(AcquireSwapchainTexture, name)       \
diff --git a/src/gpu/d3d12/SDL_gpu_d3d12.c b/src/gpu/d3d12/SDL_gpu_d3d12.c
index 36e328a7f478a..28db6c12ad88a 100644
--- a/src/gpu/d3d12/SDL_gpu_d3d12.c
+++ b/src/gpu/d3d12/SDL_gpu_d3d12.c
@@ -750,6 +750,7 @@ struct D3D12Renderer
     // FIXME: these might not be necessary since we're not using custom heaps
     bool UMA;
     bool UMACacheCoherent;
+    Uint32 allowedFramesInFlight;
 
     // Indirect command signatures
     ID3D12CommandSignature *indirectDrawCommandSignature;
@@ -6809,6 +6810,20 @@ static bool D3D12_SetSwapchainParameters(
     return true;
 }
 
+static bool D3D12_SetAllowedFramesInFlight(
+    SDL_GPURenderer *driverData,
+    Uint32 allowedFramesInFlight)
+{
+    D3D12Renderer *renderer = (D3D12Renderer *)driverData;
+
+    if (!D3D12_Wait(driverData)) {
+        return false;
+    }
+
+    renderer->allowedFramesInFlight = allowedFramesInFlight;
+    return true;
+}
+
 static SDL_GPUTextureFormat D3D12_GetSwapchainTextureFormat(
     SDL_GPURenderer *driverData,
     SDL_Window *window)
@@ -7569,7 +7584,7 @@ static bool D3D12_Submit(
 
         windowData->inFlightFences[windowData->frameCounter] = (SDL_GPUFence*)d3d12CommandBuffer->inFlightFence;
         (void)SDL_AtomicIncRef(&d3d12CommandBuffer->inFlightFence->referenceCount);
-        windowData->frameCounter = (windowData->frameCounter + 1) % MAX_FRAMES_IN_FLIGHT;
+        windowData->frameCounter = (windowData->frameCounter + 1) % renderer->allowedFramesInFlight;
     }
 
     // Check for cleanups
@@ -8181,10 +8196,10 @@ static bool D3D12_INTERNAL_TryInitializeD3D12DebugInfoQueue(D3D12Renderer *rende
 }
 
 static void WINAPI D3D12_INTERNAL_OnD3D12DebugInfoMsg(
-    D3D12_MESSAGE_CATEGORY category, 
-    D3D12_MESSAGE_SEVERITY severity, 
-    D3D12_MESSAGE_ID id, 
-    LPCSTR description, 
+    D3D12_MESSAGE_CATEGORY category,
+    D3D12_MESSAGE_SEVERITY severity,
+    D3D12_MESSAGE_ID id,
+    LPCSTR description,
     void *context)
 {
     char *catStr;
@@ -8288,7 +8303,7 @@ static void D3D12_INTERNAL_TryInitializeD3D12DebugInfoLogger(D3D12Renderer *rend
         D3D12_MESSAGE_CALLBACK_FLAG_NONE,
         NULL,
         NULL);
-    
+
     ID3D12InfoQueue1_Release(infoQueue);
 }
 #endif
@@ -8776,6 +8791,7 @@ static SDL_GPUDevice *D3D12_CreateDevice(bool debugMode, bool preferLowPower, SD
     renderer->disposeLock = SDL_CreateMutex();
 
     renderer->debug_mode = debugMode;
+    renderer->allowedFramesInFlight = 2;
 
     renderer->semantic = SDL_GetStringProperty(props, SDL_PROP_GPU_DEVICE_CREATE_D3D12_SEMANTIC_NAME_STRING, "TEXCOORD");
 
diff --git a/src/gpu/metal/SDL_gpu_metal.m b/src/gpu/metal/SDL_gpu_metal.m
index 6c496a5c11402..ebea8d5f9984a 100644
--- a/src/gpu/metal/SDL_gpu_metal.m
+++ b/src/gpu/metal/SDL_gpu_metal.m
@@ -641,6 +641,7 @@ static MTLDepthClipMode SDLToMetal_DepthClipMode(
     id<MTLCommandQueue> queue;
 
     bool debugMode;
+    Uint32 allowedFramesInFlight;
 
     MetalWindowData **claimedWindows;
     Uint32 claimedWindowCount;
@@ -3817,6 +3818,22 @@ static bool METAL_SetSwapchainParameters(
     }
 }
 
+static bool METAL_SetAllowedFramesInFlight(
+    SDL_GPURenderer *driverData,
+    Uint32 allowedFramesInFlight)
+{
+    @autoreleasepool {
+        MetalRenderer *renderer = (MetalRenderer *)driverData;
+
+        if (!METAL_Wait(driverData)) {
+            return false;
+        }
+
+        renderer->allowedFramesInFlight = allowedFramesInFlight;
+        return true;
+    }
+}
+
 // Submission
 
 static bool METAL_Submit(
@@ -3843,7 +3860,7 @@ static bool METAL_Submit(
 
             (void)SDL_AtomicIncRef(&metalCommandBuffer->fence->referenceCount);
 
-            windowData->frameCounter = (windowData->frameCounter + 1) % MAX_FRAMES_IN_FLIGHT;
+            windowData->frameCounter = (windowData->frameCounter + 1) % renderer->allowedFramesInFlight;
         }
 
         // Notify the fence when the command buffer has completed
@@ -4301,6 +4318,7 @@ static void METAL_INTERNAL_DestroyBlitResources(
 
         // Remember debug mode
         renderer->debugMode = debugMode;
+        renderer->allowedFramesInFlight = 2;
 
         // Set up colorspace array
         SwapchainCompositionToColorSpace[0] = kCGColorSpaceSRGB;
diff --git a/src/gpu/vulkan/SDL_gpu_vulkan.c b/src/gpu/vulkan/SDL_gpu_vulkan.c
index a8202b54fda01..80ca23581cf1d 100644
--- a/src/gpu/vulkan/SDL_gpu_vulkan.c
+++ b/src/gpu/vulkan/SDL_gpu_vulkan.c
@@ -1123,6 +1123,8 @@ struct VulkanRenderer
 
     bool debugMode;
     bool preferLowPower;
+    Uint32 allowedFramesInFlight;
+
     VulkanExtensions supports;
     bool supportsDebugUtils;
     bool supportsColorspace;
@@ -9898,6 +9900,20 @@ static bool VULKAN_SetSwapchainParameters(
     return true;
 }
 
+static bool VULKAN_SetAllowedFramesInFlight(
+    SDL_GPURenderer *driverData,
+    Uint32 allowedFramesInFlight)
+{
+    VulkanRenderer *renderer = (VulkanRenderer *)driverData;
+
+    if (!VULKAN_Wait(driverData)) {
+        return false;
+    }
+
+    renderer->allowedFramesInFlight = allowedFramesInFlight;
+    return true;
+}
+
 // Submission structure
 
 static VulkanFenceHandle *VULKAN_INTERNAL_AcquireFenceFromPool(
@@ -10348,8 +10364,7 @@ static bool VULKAN_Submit(
         }
 
         presentData->windowData->frameCounter =
-            (presentData->windowData->frameCounter + 1) % MAX_FRAMES_IN_FLIGHT;
-
+            (presentData->windowData->frameCounter + 1) % renderer->allowedFramesInFlight;
     }
 
     // Check if we can perform any cleanups
@@ -11438,6 +11453,7 @@ static SDL_GPUDevice *VULKAN_CreateDevice(bool debugMode, bool preferLowPower, S
     SDL_memset(renderer, '\0', sizeof(VulkanRenderer));
     renderer->debugMode = debugMode;
     renderer->preferLowPower = preferLowPower;
+    renderer->allowedFramesInFlight = 2;
 
     if (!VULKAN_INTERNAL_PrepareVulkan(renderer)) {
         SDL_free(renderer);