SDL: render: Add Suspend/Resume calls for GDK support

From ee5c5cf755f121df119c2b7fce51f96215523cd5 Mon Sep 17 00:00:00 2001
From: Ethan Lee <[EMAIL REDACTED]>
Date: Wed, 4 Mar 2026 20:26:54 -0500
Subject: [PATCH] render: Add Suspend/Resume calls for GDK support

---
 include/SDL3/SDL_main.h                  |  4 +--
 include/SDL3/SDL_render.h                | 34 ++++++++++++++++++++++++
 src/core/SDL_core_unsupported.c          |  8 ++++++
 src/core/SDL_core_unsupported.h          |  2 ++
 src/dynapi/SDL_dynapi.sym                |  2 ++
 src/dynapi/SDL_dynapi_overrides.h        |  2 ++
 src/dynapi/SDL_dynapi_procs.h            |  2 ++
 src/render/SDL_render.c                  | 20 ++++++++++++++
 src/render/SDL_sysrender.h               |  5 ++++
 src/render/direct3d12/SDL_render_d3d12.c | 30 ++++++++++-----------
 src/render/gpu/SDL_render_gpu.c          | 31 +++++++++++----------
 test/testsymbols.c                       |  2 ++
 12 files changed, 108 insertions(+), 34 deletions(-)

diff --git a/include/SDL3/SDL_main.h b/include/SDL3/SDL_main.h
index 771572f29f1aa..02733e0cc03c1 100644
--- a/include/SDL3/SDL_main.h
+++ b/include/SDL3/SDL_main.h
@@ -667,9 +667,7 @@ extern SDL_DECLSPEC void SDLCALL SDL_UnregisterApp(void);
  * This should be called from an event watch in response to an
  * `SDL_EVENT_DID_ENTER_BACKGROUND` event.
  *
- * When using SDL_Render, your event watch should be added _after_ creating
- * the `SDL_Renderer`; this allows the timing of the D3D12 command queue
- * suspension to execute in the correct order.
+ * When using SDL_Render, this should be called after calling SDL_GDKSuspendRenderer.
  *
  * When using SDL_GPU, this should be called after calling SDL_GDKSuspendGPU.
  *
diff --git a/include/SDL3/SDL_render.h b/include/SDL3/SDL_render.h
index 1f7857d9539aa..9dcd25e4b924e 100644
--- a/include/SDL3/SDL_render.h
+++ b/include/SDL3/SDL_render.h
@@ -3082,6 +3082,40 @@ extern SDL_DECLSPEC bool SDLCALL SDL_SetGPURenderState(SDL_Renderer *renderer, S
  */
 extern SDL_DECLSPEC void SDLCALL SDL_DestroyGPURenderState(SDL_GPURenderState *state);
 
+#ifdef SDL_PLATFORM_GDK
+
+/**
+ * Call this to suspend Render operations on Xbox when you receive the
+ * SDL_EVENT_DID_ENTER_BACKGROUND event.
+ *
+ * Do NOT call any SDL_Render functions after calling this function! This must
+ * also be called before calling SDL_GDKSuspendComplete.
+ *
+ * \param renderer the renderer which should suspend operation
+ *
+ * \since This function is available since SDL 3.6.0.
+ *
+ * \sa SDL_AddEventWatch
+ */
+extern SDL_DECLSPEC void SDLCALL SDL_GDKSuspendRenderer(SDL_Renderer *renderer);
+
+/**
+ * Call this to resume Render operations on Xbox when you receive the
+ * SDL_EVENT_WILL_ENTER_FOREGROUND event.
+ *
+ * When resuming, this function MUST be called before calling any other
+ * SDL_Render functions.
+ *
+ * \param renderer the renderer which should resume operation
+ *
+ * \since This function is available since SDL 3.6.0.
+ *
+ * \sa SDL_AddEventWatch
+ */
+extern SDL_DECLSPEC void SDLCALL SDL_GDKResumeRenderer(SDL_Renderer *renderer);
+
+#endif /* SDL_PLATFORM_GDK */
+
 /* Ends C function definitions when using C++ */
 #ifdef __cplusplus
 }
diff --git a/src/core/SDL_core_unsupported.c b/src/core/SDL_core_unsupported.c
index 5a70b5b4bf281..09b675d6ec171 100644
--- a/src/core/SDL_core_unsupported.c
+++ b/src/core/SDL_core_unsupported.c
@@ -69,6 +69,14 @@ void SDL_GDKResumeGPU(SDL_GPUDevice *device)
 {
 }
 
+void SDL_GDKSuspendRenderer(SDL_Renderer *renderer)
+{
+}
+
+void SDL_GDKResumeRenderer(SDL_Renderer *renderer)
+{
+}
+
 #endif /* !SDL_PLATFORM_GDK */
 
 #if !defined(SDL_PLATFORM_WINDOWS)
diff --git a/src/core/SDL_core_unsupported.h b/src/core/SDL_core_unsupported.h
index 3a9428773c680..b61a9a3a2f452 100644
--- a/src/core/SDL_core_unsupported.h
+++ b/src/core/SDL_core_unsupported.h
@@ -35,6 +35,8 @@ extern SDL_DECLSPEC void SDLCALL SDL_GDKSuspendComplete(void);
 extern SDL_DECLSPEC bool SDLCALL SDL_GetGDKDefaultUser(XUserHandle *outUserHandle);
 extern SDL_DECLSPEC void SDLCALL SDL_GDKSuspendGPU(SDL_GPUDevice *device);
 extern SDL_DECLSPEC void SDLCALL SDL_GDKResumeGPU(SDL_GPUDevice *device);
+extern SDL_DECLSPEC void SDLCALL SDL_GDKSuspendRenderer(SDL_Renderer *renderer);
+extern SDL_DECLSPEC void SDLCALL SDL_GDKResumeRenderer(SDL_Renderer *renderer);
 #endif /* !SDL_PLATFORM_GDK */
 
 #if !defined(SDL_PLATFORM_WINDOWS)
diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym
index 184f8708eabb9..ef10030d26b81 100644
--- a/src/dynapi/SDL_dynapi.sym
+++ b/src/dynapi/SDL_dynapi.sym
@@ -1283,6 +1283,8 @@ SDL3_0.0.0 {
     SDL_SetGPURenderStateSamplerBindings;
     SDL_SetGPURenderStateStorageTextures;
     SDL_SetGPURenderStateStorageBuffers;
+    SDL_GDKSuspendRenderer;
+    SDL_GDKResumeRenderer;
     # 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 87762e6d7a133..410917721d475 100644
--- a/src/dynapi/SDL_dynapi_overrides.h
+++ b/src/dynapi/SDL_dynapi_overrides.h
@@ -1309,3 +1309,5 @@
 #define SDL_SetGPURenderStateSamplerBindings SDL_SetGPURenderStateSamplerBindings_REAL
 #define SDL_SetGPURenderStateStorageTextures SDL_SetGPURenderStateStorageTextures_REAL
 #define SDL_SetGPURenderStateStorageBuffers SDL_SetGPURenderStateStorageBuffers_REAL
+#define SDL_GDKSuspendRenderer SDL_GDKSuspendRenderer_REAL
+#define SDL_GDKResumeRenderer SDL_GDKResumeRenderer_REAL
diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h
index d965c7b628d1b..175f2fd4da105 100644
--- a/src/dynapi/SDL_dynapi_procs.h
+++ b/src/dynapi/SDL_dynapi_procs.h
@@ -1317,3 +1317,5 @@ SDL_DYNAPI_PROC(SDL_Tray*,SDL_CreateTrayWithProperties,(SDL_PropertiesID a),(a),
 SDL_DYNAPI_PROC(bool,SDL_SetGPURenderStateSamplerBindings,(SDL_GPURenderState *a,int b,const SDL_GPUTextureSamplerBinding *c),(a,b,c),return)
 SDL_DYNAPI_PROC(bool,SDL_SetGPURenderStateStorageTextures,(SDL_GPURenderState *a,int b,SDL_GPUTexture *const*c),(a,b,c),return)
 SDL_DYNAPI_PROC(bool,SDL_SetGPURenderStateStorageBuffers,(SDL_GPURenderState *a,int b,SDL_GPUBuffer *const*c),(a,b,c),return)
+SDL_DYNAPI_PROC(void,SDL_GDKSuspendRenderer,(SDL_Renderer *a),(a),)
+SDL_DYNAPI_PROC(void,SDL_GDKResumeRenderer,(SDL_Renderer *a),(a),)
diff --git a/src/render/SDL_render.c b/src/render/SDL_render.c
index 684ad7b82d533..e54ff39821395 100644
--- a/src/render/SDL_render.c
+++ b/src/render/SDL_render.c
@@ -6347,3 +6347,23 @@ void SDL_DestroyGPURenderState(SDL_GPURenderState *state)
     SDL_free(state->storage_buffers);
     SDL_free(state);
 }
+
+#ifdef SDL_PLATFORM_GDK
+
+void SDLCALL SDL_GDKSuspendRenderer(SDL_Renderer *renderer)
+{
+    CHECK_RENDERER_MAGIC(renderer,);
+    if (renderer->GDKSuspendRenderer != NULL) {
+        renderer->GDKSuspendRenderer(renderer);
+    }
+}
+
+void SDLCALL SDL_GDKResumeRenderer(SDL_Renderer *renderer)
+{
+    CHECK_RENDERER_MAGIC(renderer,);
+    if (renderer->GDKResumeRenderer != NULL) {
+        renderer->GDKResumeRenderer(renderer);
+    }
+}
+
+#endif /* SDL_PLATFORM_GDK */
diff --git a/src/render/SDL_sysrender.h b/src/render/SDL_sysrender.h
index a1bb44a12d2e8..c1c13074ca42a 100644
--- a/src/render/SDL_sysrender.h
+++ b/src/render/SDL_sysrender.h
@@ -280,6 +280,11 @@ struct SDL_Renderer
 
     bool (*AddVulkanRenderSemaphores)(SDL_Renderer *renderer, Uint32 wait_stage_mask, Sint64 wait_semaphore, Sint64 signal_semaphore);
 
+#ifdef SDL_PLATFORM_GDK
+    void (*GDKSuspendRenderer)(SDL_Renderer *renderer);
+    void (*GDKResumeRenderer)(SDL_Renderer *renderer);
+#endif
+
     // The current renderer info
     const char *name;
     SDL_PixelFormat *texture_formats;
diff --git a/src/render/direct3d12/SDL_render_d3d12.c b/src/render/direct3d12/SDL_render_d3d12.c
index ff12935595ce1..3330a558a2fa2 100644
--- a/src/render/direct3d12/SDL_render_d3d12.c
+++ b/src/render/direct3d12/SDL_render_d3d12.c
@@ -563,25 +563,25 @@ static HRESULT D3D12_IssueBatch(D3D12_RenderData *data)
 }
 
 #if defined(SDL_PLATFORM_XBOXONE) || defined(SDL_PLATFORM_XBOXSERIES)
-static bool SDLCALL D3D12_GDKEventFilter(void* userdata, SDL_Event* event)
+
+static void D3D12_GDKSuspendRenderer(SDL_Renderer *renderer)
 {
-    D3D12_RenderData *data = (D3D12_RenderData *)userdata;
-    if (event->type == SDL_EVENT_DID_ENTER_BACKGROUND) {
-        data->commandQueue->SuspendX(0);
-    } else if (event->type == SDL_EVENT_WILL_ENTER_FOREGROUND) {
-        data->commandQueue->ResumeX();
-    }
-    return true;
+    D3D12_RenderData *data = (D3D12_RenderData *)renderer->internal;
+    data->commandQueue->SuspendX(0);
+}
+
+static void D3D12_GDKResumeRenderer(SDL_Renderer *renderer)
+{
+    D3D12_RenderData *data = (D3D12_RenderData *)renderer->internal;
+    data->commandQueue->ResumeX();
 }
+
 #endif
 
 static void D3D12_DestroyRenderer(SDL_Renderer *renderer)
 {
     D3D12_RenderData *data = (D3D12_RenderData *)renderer->internal;
     if (data) {
-#if defined(SDL_PLATFORM_XBOXONE) || defined(SDL_PLATFORM_XBOXSERIES)
-        SDL_RemoveEventWatch(D3D12_GDKEventFilter, data);
-#endif
         D3D12_WaitForGPU(data);
         D3D12_ReleaseAll(renderer);
         SDL_free(data);
@@ -1128,10 +1128,6 @@ static HRESULT D3D12_CreateDeviceResources(SDL_Renderer *renderer)
     SDL_SetPointerProperty(props, SDL_PROP_RENDERER_D3D12_DEVICE_POINTER, data->d3dDevice);
     SDL_SetPointerProperty(props, SDL_PROP_RENDERER_D3D12_COMMAND_QUEUE_POINTER, data->commandQueue);
 
-#if defined(SDL_PLATFORM_XBOXONE) || defined(SDL_PLATFORM_XBOXSERIES)
-    SDL_AddEventWatch(D3D12_GDKEventFilter, data);
-#endif
-
 done:
     D3D_SAFE_RELEASE(d3dDevice);
     return result;
@@ -3517,6 +3513,10 @@ bool D3D12_CreateRenderer(SDL_Renderer *renderer, SDL_Window *window, SDL_Proper
     renderer->DestroyTexture = D3D12_DestroyTexture;
     renderer->DestroyRenderer = D3D12_DestroyRenderer;
     renderer->SetVSync = D3D12_SetVSync;
+#if defined(SDL_PLATFORM_XBOXONE) || defined(SDL_PLATFORM_XBOXSERIES)
+    renderer->GDKSuspendRenderer = D3D12_GDKSuspendRenderer;
+    renderer->GDKResumeRenderer = D3D12_GDKResumeRenderer;
+#endif
     renderer->internal = data;
     D3D12_InvalidateCachedState(renderer);
 
diff --git a/src/render/gpu/SDL_render_gpu.c b/src/render/gpu/SDL_render_gpu.c
index 5887023cda10a..b9b267324fa51 100644
--- a/src/render/gpu/SDL_render_gpu.c
+++ b/src/render/gpu/SDL_render_gpu.c
@@ -1563,17 +1563,19 @@ static void GPU_DestroyTexture(SDL_Renderer *renderer, SDL_Texture *texture)
 }
 
 #ifdef SDL_PLATFORM_GDK
-static bool SDLCALL GPU_GDKEventFilter(void *userdata, SDL_Event *event)
+
+static void GPU_GDKSuspendRenderer(SDL_Renderer *renderer)
 {
-    GPU_RenderData *data = (GPU_RenderData *)userdata;
-    SDL_assert(!data->external_device);
-    if (event->type == SDL_EVENT_DID_ENTER_BACKGROUND) {
-        SDL_GDKSuspendGPU(data->device);
-    } else if (event->type == SDL_EVENT_WILL_ENTER_FOREGROUND) {
-        SDL_GDKResumeGPU(data->device);
-    }
-    return true;
+    GPU_RenderData *data = (GPU_RenderData *)renderer->internal;
+    SDL_GDKSuspendGPU(data->device);
+}
+
+static void GPU_GDKResumeRenderer(SDL_Renderer *renderer)
+{
+    GPU_RenderData *data = (GPU_RenderData *)renderer->internal;
+    SDL_GDKResumeGPU(data->device);
 }
+
 #endif
 
 static void GPU_DestroyRenderer(SDL_Renderer *renderer)
@@ -1609,9 +1611,6 @@ static void GPU_DestroyRenderer(SDL_Renderer *renderer)
     if (data->device) {
         GPU_ReleaseShaders(&data->shaders, data->device);
         if (!data->external_device) {
-#ifdef SDL_PLATFORM_GDK
-            SDL_RemoveEventWatch(GPU_GDKEventFilter, data);
-#endif
             SDL_DestroyGPUDevice(data->device);
         }
     }
@@ -1722,6 +1721,10 @@ static bool GPU_CreateRenderer(SDL_Renderer *renderer, SDL_Window *window, SDL_P
     renderer->DestroyTexture = GPU_DestroyTexture;
     renderer->DestroyRenderer = GPU_DestroyRenderer;
     renderer->SetVSync = GPU_SetVSync;
+#ifdef SDL_PLATFORM_GDK
+    renderer->GDKSuspendRenderer = GPU_GDKSuspendRenderer;
+    renderer->GDKResumeRenderer = GPU_GDKResumeRenderer;
+#endif
     renderer->internal = data;
     renderer->window = window;
     renderer->name = GPU_RenderDriver.name;
@@ -1775,10 +1778,6 @@ static bool GPU_CreateRenderer(SDL_Renderer *renderer, SDL_Window *window, SDL_P
         if (!data->device) {
             return false;
         }
-
-#ifdef SDL_PLATFORM_GDK
-        SDL_AddEventWatch(GPU_GDKEventFilter, data);
-#endif
     }
 
     if (!GPU_InitShaders(&data->shaders, data->device)) {
diff --git a/test/testsymbols.c b/test/testsymbols.c
index 70f14fb11fc36..cf363e6251d33 100644
--- a/test/testsymbols.c
+++ b/test/testsymbols.c
@@ -40,6 +40,8 @@ extern SDL_DECLSPEC void SDLCALL SDL_GDKSuspendGPU(void);
 extern SDL_DECLSPEC void SDLCALL SDL_GDKSuspendComplete(void);
 extern SDL_DECLSPEC void SDLCALL SDL_GetGDKDefaultUser(void);
 extern SDL_DECLSPEC void SDLCALL SDL_GetGDKTaskQueue(void);
+extern SDL_DECLSPEC void SDLCALL SDL_GDKSuspendRenderer(void);
+extern SDL_DECLSPEC void SDLCALL SDL_GDKResumeRenderer(void);
 #endif
 
 #if !defined(SDL_PLATFORM_IOS)