SDL: Add SDL_CreateGPURenderer

From f7b7188837e2d61373846ad28448935c1548e3f5 Mon Sep 17 00:00:00 2001
From: Topi-Matti Ritala <[EMAIL REDACTED]>
Date: Mon, 7 Apr 2025 18:19:27 +0300
Subject: [PATCH] Add SDL_CreateGPURenderer

---
 include/SDL3/SDL_render.h         | 34 +++++++++++++++++++++++++++++++
 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           | 31 ++++++++++++++++++++++++++++
 src/render/gpu/SDL_render_gpu.c   | 13 ++++++++----
 src/render/gpu/SDL_shaders_gpu.c  | 21 ++++++++++++++++---
 test/testgpurender_effects.c      | 10 ++-------
 8 files changed, 97 insertions(+), 15 deletions(-)

diff --git a/include/SDL3/SDL_render.h b/include/SDL3/SDL_render.h
index 0b6e7fbb340d0..17bed4514dd09 100644
--- a/include/SDL3/SDL_render.h
+++ b/include/SDL3/SDL_render.h
@@ -283,6 +283,12 @@ extern SDL_DECLSPEC SDL_Renderer * SDLCALL SDL_CreateRenderer(SDL_Window *window
  *   present synchronized with the refresh rate. This property can take any
  *   value that is supported by SDL_SetRenderVSync() for the renderer.
  *
+ * With the SDL GPU renderer:
+ *
+ * - `SDL_PROP_RENDERER_CREATE_GPU_SHADERS_SPIRV_BOOLEAN`: the app is able to provide SPIR-V shaders to SDL_GPURenderState, optional.
+ * - `SDL_PROP_RENDERER_CREATE_GPU_SHADERS_DXIL_BOOLEAN`: the app is able to provide DXIL shaders to SDL_GPURenderState, optional.
+ * - `SDL_PROP_RENDERER_CREATE_GPU_SHADERS_MSL_BOOLEAN`: the app is able to provide MSL shaders to SDL_GPURenderState, optional.
+ *
  * With the vulkan renderer:
  *
  * - `SDL_PROP_RENDERER_CREATE_VULKAN_INSTANCE_POINTER`: the VkInstance to use
@@ -319,6 +325,9 @@ extern SDL_DECLSPEC SDL_Renderer * SDLCALL SDL_CreateRendererWithProperties(SDL_
 #define SDL_PROP_RENDERER_CREATE_SURFACE_POINTER                            "SDL.renderer.create.surface"
 #define SDL_PROP_RENDERER_CREATE_OUTPUT_COLORSPACE_NUMBER                   "SDL.renderer.create.output_colorspace"
 #define SDL_PROP_RENDERER_CREATE_PRESENT_VSYNC_NUMBER                       "SDL.renderer.create.present_vsync"
+#define SDL_PROP_RENDERER_CREATE_GPU_SHADERS_SPIRV_BOOLEAN                  "SDL.renderer.create.gpu.shaders_spirv"
+#define SDL_PROP_RENDERER_CREATE_GPU_SHADERS_DXIL_BOOLEAN                   "SDL.renderer.create.gpu.shaders_dxil"
+#define SDL_PROP_RENDERER_CREATE_GPU_SHADERS_MSL_BOOLEAN                    "SDL.renderer.create.gpu.shaders_msl"
 #define SDL_PROP_RENDERER_CREATE_VULKAN_INSTANCE_POINTER                    "SDL.renderer.create.vulkan.instance"
 #define SDL_PROP_RENDERER_CREATE_VULKAN_SURFACE_NUMBER                      "SDL.renderer.create.vulkan.surface"
 #define SDL_PROP_RENDERER_CREATE_VULKAN_PHYSICAL_DEVICE_POINTER             "SDL.renderer.create.vulkan.physical_device"
@@ -326,6 +335,31 @@ extern SDL_DECLSPEC SDL_Renderer * SDLCALL SDL_CreateRendererWithProperties(SDL_
 #define SDL_PROP_RENDERER_CREATE_VULKAN_GRAPHICS_QUEUE_FAMILY_INDEX_NUMBER  "SDL.renderer.create.vulkan.graphics_queue_family_index"
 #define SDL_PROP_RENDERER_CREATE_VULKAN_PRESENT_QUEUE_FAMILY_INDEX_NUMBER   "SDL.renderer.create.vulkan.present_queue_family_index"
 
+/**
+ * Create a 2D GPU rendering context for a window, with support for the specified shader format.
+ *
+ * This is a convenience function to create a SDL GPU backed renderer, intended to be used with SDL_GPURenderState.
+ * The resulting renderer will support shaders in one of the specified shader formats.
+ *
+ * If no available GPU driver supports any of the specified shader formats, this function will fail.
+ *
+ * \param window the window where rendering is displayed.
+ * \param format_flags a bitflag indicating which shader formats the app is able to provide.
+ * \param device a pointer filled with the associated GPU device, or NULL on error.
+ * \returns a valid rendering context or NULL if there was an error; call SDL_GetError() for more information.
+ *
+ * \threadsafety This function should only be called on the main thread.
+ *
+ * \since This function is available since SDL 3.4.0.
+ *
+ * \sa SDL_CreateRendererWithProperties
+ * \sa SDL_GetGPUShaderFormats
+ * \sa SDL_CreateGPUShader
+ * \sa SDL_CreateGPURenderState
+ * \sa SDL_SetRenderGPUState
+ */
+extern SDL_DECLSPEC SDL_Renderer * SDLCALL SDL_CreateGPURenderer(SDL_Window *window, SDL_GPUShaderFormat format_flags, SDL_GPUDevice **device);
+
 /**
  * Create a 2D software rendering context for a surface.
  *
diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym
index c53dcd7793d1b..44f9d0e062a36 100644
--- a/src/dynapi/SDL_dynapi.sym
+++ b/src/dynapi/SDL_dynapi.sym
@@ -1249,6 +1249,7 @@ SDL3_0.0.0 {
     SDL_SetRenderTextureAddressMode;
     SDL_GetRenderTextureAddressMode;
     SDL_GetGPUDeviceProperties;
+    SDL_CreateGPURenderer;
     # 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 6fa3944607a03..a12f3fbf24b4a 100644
--- a/src/dynapi/SDL_dynapi_overrides.h
+++ b/src/dynapi/SDL_dynapi_overrides.h
@@ -1274,3 +1274,4 @@
 #define SDL_SetRenderTextureAddressMode SDL_SetRenderTextureAddressMode_REAL
 #define SDL_GetRenderTextureAddressMode SDL_GetRenderTextureAddressMode_REAL
 #define SDL_GetGPUDeviceProperties SDL_GetGPUDeviceProperties_REAL
+#define SDL_CreateGPURenderer SDL_CreateGPURenderer_REAL
diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h
index 8c1158e186626..d7988ac2b0944 100644
--- a/src/dynapi/SDL_dynapi_procs.h
+++ b/src/dynapi/SDL_dynapi_procs.h
@@ -1282,3 +1282,4 @@ SDL_DYNAPI_PROC(float,SDL_GetWindowProgressValue,(SDL_Window *a),(a),return)
 SDL_DYNAPI_PROC(bool,SDL_SetRenderTextureAddressMode,(SDL_Renderer *a,SDL_TextureAddressMode b,SDL_TextureAddressMode c),(a,b,c),return)
 SDL_DYNAPI_PROC(bool,SDL_GetRenderTextureAddressMode,(SDL_Renderer *a,SDL_TextureAddressMode *b,SDL_TextureAddressMode *c),(a,b,c),return)
 SDL_DYNAPI_PROC(SDL_PropertiesID,SDL_GetGPUDeviceProperties,(SDL_GPUDevice *a),(a),return)
+SDL_DYNAPI_PROC(SDL_Renderer*,SDL_CreateGPURenderer,(SDL_Window *a,SDL_GPUShaderFormat b,SDL_GPUDevice **c),(a,b,c),return)
diff --git a/src/render/SDL_render.c b/src/render/SDL_render.c
index 80b0cfd295844..3cb5c8d2b843e 100644
--- a/src/render/SDL_render.c
+++ b/src/render/SDL_render.c
@@ -1187,6 +1187,37 @@ SDL_Renderer *SDL_CreateRenderer(SDL_Window *window, const char *name)
     return renderer;
 }
 
+SDL_Renderer *SDL_CreateGPURenderer(SDL_Window *window, SDL_GPUShaderFormat format_flags, SDL_GPUDevice **device)
+{
+    if (!device) {
+        SDL_InvalidParamError("device");
+        return NULL;
+    }
+
+    *device = NULL;
+    SDL_Renderer *renderer;
+
+    SDL_PropertiesID props = SDL_CreateProperties();
+    SDL_SetPointerProperty(props, SDL_PROP_RENDERER_CREATE_WINDOW_POINTER, window);
+    if (format_flags & SDL_GPU_SHADERFORMAT_SPIRV) {
+        SDL_SetBooleanProperty(props, SDL_PROP_RENDERER_CREATE_GPU_SHADERS_SPIRV_BOOLEAN, true);
+    }
+    if (format_flags & SDL_GPU_SHADERFORMAT_DXIL) {
+        SDL_SetBooleanProperty(props, SDL_PROP_RENDERER_CREATE_GPU_SHADERS_DXIL_BOOLEAN, true);
+    }
+    if (format_flags & SDL_GPU_SHADERFORMAT_MSL) {
+        SDL_SetBooleanProperty(props, SDL_PROP_RENDERER_CREATE_GPU_SHADERS_MSL_BOOLEAN, true);
+    }
+    SDL_SetStringProperty(props, SDL_PROP_RENDERER_CREATE_NAME_STRING, "gpu");
+
+    renderer = SDL_CreateRendererWithProperties(props);
+    if (renderer) {
+        *device = (SDL_GPUDevice *)SDL_GetPointerProperty(SDL_GetRendererProperties(renderer), SDL_PROP_RENDERER_GPU_DEVICE_POINTER, NULL);
+    }
+    SDL_DestroyProperties(props);
+    return renderer;
+}
+
 SDL_Renderer *SDL_CreateSoftwareRenderer(SDL_Surface *surface)
 {
 #ifdef SDL_VIDEO_RENDER_SW
diff --git a/src/render/gpu/SDL_render_gpu.c b/src/render/gpu/SDL_render_gpu.c
index 35a5e71398776..1b4ca96c55a11 100644
--- a/src/render/gpu/SDL_render_gpu.c
+++ b/src/render/gpu/SDL_render_gpu.c
@@ -1128,21 +1128,26 @@ static void GPU_DestroyRenderer(SDL_Renderer *renderer)
     }
 
     for (Uint32 i = 0; i < SDL_arraysize(data->samplers); ++i) {
-        SDL_ReleaseGPUSampler(data->device, data->samplers[i]);
+        if (data->samplers[i]) {
+            SDL_ReleaseGPUSampler(data->device, data->samplers[i]);
+        }
     }
 
     if (data->backbuffer.texture) {
         SDL_ReleaseGPUTexture(data->device, data->backbuffer.texture);
     }
 
-    if (renderer->window) {
+    if (renderer->window && data->device) {
         SDL_ReleaseWindowFromGPUDevice(data->device, renderer->window);
     }
 
     ReleaseVertexBuffer(data);
     GPU_DestroyPipelineCache(&data->pipeline_cache);
-    GPU_ReleaseShaders(&data->shaders, data->device);
-    SDL_DestroyGPUDevice(data->device);
+
+    if (data->device) {
+        GPU_ReleaseShaders(&data->shaders, data->device);
+        SDL_DestroyGPUDevice(data->device);
+    }
 
     SDL_free(data);
 }
diff --git a/src/render/gpu/SDL_shaders_gpu.c b/src/render/gpu/SDL_shaders_gpu.c
index f09a40e46878d..e4dc5e890babd 100644
--- a/src/render/gpu/SDL_shaders_gpu.c
+++ b/src/render/gpu/SDL_shaders_gpu.c
@@ -244,9 +244,24 @@ SDL_GPUShader *GPU_GetFragmentShader(GPU_Shaders *shaders, GPU_FragmentShaderID
 
 void GPU_FillSupportedShaderFormats(SDL_PropertiesID props)
 {
-    SDL_SetBooleanProperty(props, SDL_PROP_GPU_DEVICE_CREATE_SHADERS_SPIRV_BOOLEAN, HAVE_SPIRV_SHADERS);
-    SDL_SetBooleanProperty(props, SDL_PROP_GPU_DEVICE_CREATE_SHADERS_DXIL_BOOLEAN, HAVE_DXIL60_SHADERS);
-    SDL_SetBooleanProperty(props, SDL_PROP_GPU_DEVICE_CREATE_SHADERS_MSL_BOOLEAN, HAVE_METAL_SHADERS);
+    bool custom_shaders = false;
+    if (SDL_GetBooleanProperty(props, SDL_PROP_RENDERER_CREATE_GPU_SHADERS_SPIRV_BOOLEAN, false)) {
+        SDL_SetBooleanProperty(props, SDL_PROP_GPU_DEVICE_CREATE_SHADERS_SPIRV_BOOLEAN, HAVE_SPIRV_SHADERS);
+        custom_shaders = true;
+    }
+    if (SDL_GetBooleanProperty(props, SDL_PROP_RENDERER_CREATE_GPU_SHADERS_DXIL_BOOLEAN, false)) {
+        SDL_SetBooleanProperty(props, SDL_PROP_GPU_DEVICE_CREATE_SHADERS_DXIL_BOOLEAN, HAVE_DXIL60_SHADERS);
+        custom_shaders = true;
+    }
+    if (SDL_GetBooleanProperty(props, SDL_PROP_RENDERER_CREATE_GPU_SHADERS_MSL_BOOLEAN, false)) {
+        SDL_SetBooleanProperty(props, SDL_PROP_GPU_DEVICE_CREATE_SHADERS_MSL_BOOLEAN, HAVE_METAL_SHADERS);
+        custom_shaders = true;
+    }
+    if (!custom_shaders) {
+        SDL_SetBooleanProperty(props, SDL_PROP_GPU_DEVICE_CREATE_SHADERS_SPIRV_BOOLEAN, HAVE_SPIRV_SHADERS);
+        SDL_SetBooleanProperty(props, SDL_PROP_GPU_DEVICE_CREATE_SHADERS_DXIL_BOOLEAN, HAVE_DXIL60_SHADERS);
+        SDL_SetBooleanProperty(props, SDL_PROP_GPU_DEVICE_CREATE_SHADERS_MSL_BOOLEAN, HAVE_METAL_SHADERS);
+    }
 }
 
 #endif // SDL_VIDEO_RENDER_GPU
diff --git a/test/testgpurender_effects.c b/test/testgpurender_effects.c
index 62df028dabda0..b10700b743747 100644
--- a/test/testgpurender_effects.c
+++ b/test/testgpurender_effects.c
@@ -148,12 +148,6 @@ static bool InitGPURenderState(void)
     SDL_GPURenderStateDesc desc;
     int i;
 
-    device = (SDL_GPUDevice *)SDL_GetPointerProperty(SDL_GetRendererProperties(renderer), SDL_PROP_RENDERER_GPU_DEVICE_POINTER, NULL);
-    if (!device) {
-        SDL_Log("Couldn't get GPU device");
-        return false;
-    }
-
     formats = SDL_GetGPUShaderFormats(device);
     if (formats == SDL_GPU_SHADERFORMAT_INVALID) {
         SDL_Log("Couldn't get supported shader formats: %s", SDL_GetError());
@@ -250,8 +244,8 @@ SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
         return SDL_APP_FAILURE;
     }
 
-    renderer = SDL_CreateRenderer(window, "gpu");
-    if (!renderer) {
+    renderer = SDL_CreateGPURenderer(window, SDL_GPU_SHADERFORMAT_SPIRV | SDL_GPU_SHADERFORMAT_DXIL | SDL_GPU_SHADERFORMAT_MSL, &device);
+    if (!renderer || !device) {
         SDL_Log("Couldn't create renderer: %s", SDL_GetError());
         return SDL_APP_FAILURE;
     }