SDL_image: Add helpers for loading GPU textures (closes #546)

From 4db0d16f1520724cac50806342af586736a3d347 Mon Sep 17 00:00:00 2001
From: Jaan Soulier <[EMAIL REDACTED]>
Date: Sun, 11 Jan 2026 17:38:49 -0500
Subject: [PATCH] Add helpers for loading GPU textures (closes #546)

---
 CMakeLists.txt                 |   2 +
 examples/showgpuimage.c        | 168 +++++++++++++++++++++++++++++++++
 include/SDL3_image/SDL_image.h | 135 ++++++++++++++++++++++++--
 src/IMG_gpu.c                  | 141 +++++++++++++++++++++++++++
 src/SDL_image.exports          |   3 +
 src/SDL_image.sym              |   3 +
 6 files changed, 446 insertions(+), 6 deletions(-)
 create mode 100644 examples/showgpuimage.c
 create mode 100644 src/IMG_gpu.c

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 66bc3153..d79756c2 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -270,6 +270,7 @@ add_library(${sdl3_image_target_name}
     src/IMG_avif.c
     src/IMG_bmp.c
     src/IMG_gif.c
+    src/IMG_gpu.c
     src/IMG_jpg.c
     src/IMG_jxl.c
     src/IMG_lbm.c
@@ -1330,6 +1331,7 @@ if(SDLIMAGE_SAMPLES)
     add_sdl_image_example_executable(showanim examples/showanim.c)
     add_sdl_image_example_executable(showimage examples/showimage.c)
     add_sdl_image_example_executable(showclipboard examples/showclipboard.c)
+    add_sdl_image_example_executable(showgpuimage examples/showgpuimage.c)
 endif()
 
 if(SDLIMAGE_TESTS)
diff --git a/examples/showgpuimage.c b/examples/showgpuimage.c
new file mode 100644
index 00000000..f01c8e24
--- /dev/null
+++ b/examples/showgpuimage.c
@@ -0,0 +1,168 @@
+/*
+  showgpuimage:  A test application for the SDL image loading library.
+  Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+
+#include <SDL3/SDL.h>
+#include <SDL3/SDL_main.h>
+#include <SDL3_image/SDL_image.h>
+
+static SDL_Window *window;
+static SDL_GPUDevice *device;
+static SDL_GPUTexture *texture;
+static Uint32 texture_width;
+static Uint32 texture_height;
+
+static const char *get_file_path(const char *file)
+{
+    static char path[4096];
+
+    if (*file != '/' && !SDL_GetPathInfo(file, NULL)) {
+        SDL_snprintf(path, sizeof(path), "%s%s", SDL_GetBasePath(), file);
+        if (SDL_GetPathInfo(path, NULL)) {
+            return path;
+        }
+    }
+    return file;
+}
+
+static bool load_image(SDL_GPUCommandBuffer *command_buffer, const char *path)
+{
+    SDL_ReleaseGPUTexture(device, texture);
+    texture = NULL;
+
+    SDL_GPUCopyPass *copy_pass = SDL_BeginGPUCopyPass(command_buffer);
+    if (!copy_pass) {
+        SDL_Log("SDL_BeginGPUCopyPass() failed: %s", SDL_GetError());
+        return false;
+    }
+    texture = IMG_LoadGPUTexture(device, copy_pass, get_file_path(path), &texture_width, &texture_height);
+    if (!texture) {
+        SDL_Log("IMG_LoadGPUTexture() failed: %s", SDL_GetError());
+    }
+    SDL_EndGPUCopyPass(copy_pass);
+
+    return texture != NULL;
+}
+
+int main(int argc, char *argv[])
+{
+    int result = 0;
+    bool quit = false;
+    SDL_GPUCommandBuffer *command_buffer = NULL;
+    SDL_Event event = {0};
+    SDL_GPUTexture *swapchain_texture = NULL;
+    Uint32 swapchain_width = 0;
+    Uint32 swapchain_height = 0;
+    SDL_GPUBlitInfo blit_info = {0};
+
+    if (!SDL_Init(SDL_INIT_VIDEO)) {
+        SDL_Log("SDL_Init(SDL_INIT_VIDEO) failed: %s", SDL_GetError());
+        result = 2;
+        goto done;
+    }
+
+    window = SDL_CreateWindow("", 960, 720, SDL_WINDOW_RESIZABLE);
+    if (!window) {
+        SDL_Log("SDL_CreateWindow() failed: %s", SDL_GetError());
+        result = 2;
+        goto done;
+    }
+
+    device = SDL_CreateGPUDevice(SDL_GPU_SHADERFORMAT_SPIRV | SDL_GPU_SHADERFORMAT_DXIL | SDL_GPU_SHADERFORMAT_MSL, true, NULL);
+    if (!window) {
+        SDL_Log("SDL_CreateGPUDevice() failed: %s", SDL_GetError());
+        result = 2;
+        goto done;
+    }
+
+    if (!SDL_ClaimWindowForGPUDevice(device, window)) {
+        SDL_Log("SDL_ClaimWindowForGPUDevice() failed: %s", SDL_GetError());
+        result = 2;
+        goto done;
+    }
+
+    if (argc > 1) {
+        SDL_GPUCommandBuffer *command_buffer = SDL_AcquireGPUCommandBuffer(device);
+        if (!command_buffer) {
+            SDL_Log("SDL_AcquireGPUCommandBuffer() failed: %s", SDL_GetError());
+            result = 2;
+            goto done;
+        }
+        if (!load_image(command_buffer, argv[1])) {
+            result = 2;
+            goto done;
+        }
+        SDL_SubmitGPUCommandBuffer(command_buffer);
+    }
+
+    while (!quit) {
+        command_buffer = SDL_AcquireGPUCommandBuffer(device);
+        if (!command_buffer) {
+            SDL_Log("SDL_AcquireGPUCommandBuffer() failed: %s", SDL_GetError());
+            continue;
+        }
+
+        while (SDL_PollEvent(&event)) {
+            switch (event.type) {
+            case SDL_EVENT_QUIT:
+                quit = true;
+                break;
+            case SDL_EVENT_DROP_FILE:
+                load_image(command_buffer, event.drop.data);
+                break;
+            }
+        }
+        if (quit) {
+            break;
+        }
+
+        if (!SDL_WaitAndAcquireGPUSwapchainTexture(command_buffer, window, &swapchain_texture, &swapchain_width, &swapchain_height)) {
+            SDL_Log("SDL_WaitAndAcquireGPUSwapchainTexture() failed: %s", SDL_GetError());
+            SDL_CancelGPUCommandBuffer(command_buffer);
+            continue;
+        }
+
+        if (!swapchain_texture || !swapchain_width || !swapchain_height) {
+            // Not an error. Happens on minimize
+            SDL_CancelGPUCommandBuffer(command_buffer);
+            continue;
+        }
+
+        if (texture) {
+            blit_info.source.texture = texture;
+            blit_info.source.w = texture_width;
+            blit_info.source.h = texture_height;
+            blit_info.destination.texture = swapchain_texture;
+            blit_info.destination.w = swapchain_width;
+            blit_info.destination.h = swapchain_height;
+            SDL_BlitGPUTexture(command_buffer, &blit_info);
+        }
+
+        SDL_SubmitGPUCommandBuffer(command_buffer);
+    }
+
+done:
+    SDL_ReleaseGPUTexture(device, texture);
+    SDL_ReleaseWindowFromGPUDevice(device, window);
+    SDL_DestroyGPUDevice(device);
+    SDL_DestroyWindow(window);
+    SDL_Quit();
+    return result;
+}
diff --git a/include/SDL3_image/SDL_image.h b/include/SDL3_image/SDL_image.h
index 55517e66..334d05ea 100644
--- a/include/SDL3_image/SDL_image.h
+++ b/include/SDL3_image/SDL_image.h
@@ -229,7 +229,7 @@ extern SDL_DECLSPEC SDL_Surface * SDLCALL IMG_Load_IO(SDL_IOStream *src, bool cl
 extern SDL_DECLSPEC SDL_Surface * SDLCALL IMG_LoadTyped_IO(SDL_IOStream *src, bool closeio, const char *type);
 
 /**
- * Load an image from a filesystem path into a GPU texture.
+ * Load an image from a filesystem path into a texture.
  *
  * An SDL_Texture represents an image in GPU memory, usable by SDL's 2D Render
  * API. This can be significantly more efficient than using a CPU-bound
@@ -252,7 +252,7 @@ extern SDL_DECLSPEC SDL_Surface * SDLCALL IMG_LoadTyped_IO(SDL_IOStream *src, bo
  * When done with the returned texture, the app should dispose of it with a
  * call to SDL_DestroyTexture().
  *
- * \param renderer the SDL_Renderer to use to create the GPU texture.
+ * \param renderer the SDL_Renderer to use to create the texture.
  * \param file a path on the filesystem to load an image from.
  * \returns a new texture, or NULL on error.
  *
@@ -264,7 +264,7 @@ extern SDL_DECLSPEC SDL_Surface * SDLCALL IMG_LoadTyped_IO(SDL_IOStream *src, bo
 extern SDL_DECLSPEC SDL_Texture * SDLCALL IMG_LoadTexture(SDL_Renderer *renderer, const char *file);
 
 /**
- * Load an image from an SDL data source into a GPU texture.
+ * Load an image from an SDL data source into a texture.
  *
  * An SDL_Texture represents an image in GPU memory, usable by SDL's 2D Render
  * API. This can be significantly more efficient than using a CPU-bound
@@ -296,7 +296,7 @@ extern SDL_DECLSPEC SDL_Texture * SDLCALL IMG_LoadTexture(SDL_Renderer *renderer
  * When done with the returned texture, the app should dispose of it with a
  * call to SDL_DestroyTexture().
  *
- * \param renderer the SDL_Renderer to use to create the GPU texture.
+ * \param renderer the SDL_Renderer to use to create the texture.
  * \param src an SDL_IOStream that data will be read from.
  * \param closeio true to close/free the SDL_IOStream before returning, false
  *                to leave it open.
@@ -310,7 +310,7 @@ extern SDL_DECLSPEC SDL_Texture * SDLCALL IMG_LoadTexture(SDL_Renderer *renderer
 extern SDL_DECLSPEC SDL_Texture * SDLCALL IMG_LoadTexture_IO(SDL_Renderer *renderer, SDL_IOStream *src, bool closeio);
 
 /**
- * Load an image from an SDL data source into a GPU texture.
+ * Load an image from an SDL data source into a texture.
  *
  * An SDL_Texture represents an image in GPU memory, usable by SDL's 2D Render
  * API. This can be significantly more efficient than using a CPU-bound
@@ -348,7 +348,7 @@ extern SDL_DECLSPEC SDL_Texture * SDLCALL IMG_LoadTexture_IO(SDL_Renderer *rende
  * When done with the returned texture, the app should dispose of it with a
  * call to SDL_DestroyTexture().
  *
- * \param renderer the SDL_Renderer to use to create the GPU texture.
+ * \param renderer the SDL_Renderer to use to create the texture.
  * \param src an SDL_IOStream that data will be read from.
  * \param closeio true to close/free the SDL_IOStream before returning, false
  *                to leave it open.
@@ -363,6 +363,129 @@ extern SDL_DECLSPEC SDL_Texture * SDLCALL IMG_LoadTexture_IO(SDL_Renderer *rende
  */
 extern SDL_DECLSPEC SDL_Texture * SDLCALL IMG_LoadTextureTyped_IO(SDL_Renderer *renderer, SDL_IOStream *src, bool closeio, const char *type);
 
+/**
+ * Load an image from a filesystem path into a GPU texture.
+ *
+ * An SDL_GPUTexture represents an image in GPU memory, usable by SDL's GPU
+ * API. Regardless of the source format of the image, this function will create a
+ * GPU texture with the format SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM with no mip
+ * levels. It can be bound as a sampled texture from a graphics or compute pipeline
+ * and as a a readonly storage texture in a compute pipeline.
+ * 
+ * There is a separate function to read files from an SDL_IOStream, if you
+ * need an i/o abstraction to provide data from anywhere instead of a simple
+ * filesystem read; that function is IMG_LoadGPUTexture_IO().
+ *
+ * When done with the returned texture, the app should dispose of it with a
+ * call to SDL_ReleaseGPUTexture().
+ *
+ * \param device the SDL_GPUDevice to use to create the GPU texture.
+ * \param copy_pass the SDL_GPUCopyPass to use to upload the loaded image to
+ *                  the GPU texture.
+ * \param file a path on the filesystem to load an image from.
+ * \param width a pointer filled in with the width of the GPU texture. may be NULL.
+ * \param height a pointer filled in with the width of the GPU texture. may be NULL.
+ * \returns a new GPU texture, or NULL on error.
+ *
+ * \since This function is available since SDL_image 3.4.0.
+ *
+ * \sa IMG_LoadGPUTextureTyped_IO
+ * \sa IMG_LoadGPUTexture_IO
+ */
+extern SDL_DECLSPEC SDL_GPUTexture * SDLCALL IMG_LoadGPUTexture(SDL_GPUDevice *device, SDL_GPUCopyPass *copy_pass, const char *file, int *width, int *height);
+
+/**
+ * Load an image from an SDL data source into a GPU texture.
+ *
+ * An SDL_GPUTexture represents an image in GPU memory, usable by SDL's GPU
+ * API. Regardless of the source format of the image, this function will create a
+ * GPU texture with the format SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM with no mip
+ * levels. It can be bound as a sampled texture from a graphics or compute pipeline
+ * and as a a readonly storage texture in a compute pipeline.
+ *
+ * If `closeio` is true, `src` will be closed before returning, whether this
+ * function succeeds or not. SDL_image reads everything it needs from `src`
+ * during this call in any case.
+ *
+ * There is a separate function to read files from disk without having to deal
+ * with SDL_IOStream: `IMG_LoadGPUTexture(device, copy_pass, "filename.jpg", width, height)
+ * will call this function and manage those details for you, determining the file type
+ * from the filename's extension.
+ *
+ * There is also IMG_LoadGPUTextureTyped_IO(), which is equivalent to this
+ * function except a file extension (like "BMP", "JPG", etc) can be specified,
+ * in case SDL_image cannot autodetect the file format.
+ *
+ * When done with the returned texture, the app should dispose of it with a
+ * call to SDL_ReleaseGPUTexture().
+ *
+ * \param device the SDL_GPUDevice to use to create the GPU texture.
+ * \param copy_pass the SDL_GPUCopyPass to use to upload the loaded image to
+ *                  the GPU texture.
+ * \param src an SDL_IOStream that data will be read from.
+ * \param closeio true to close/free the SDL_IOStream before returning, false
+ *                to leave it open.
+ * \param width a pointer filled in with the width of the GPU texture. may be NULL.
+ * \param height a pointer filled in with the width of the GPU texture. may be NULL.
+ * \returns a new GPU texture, or NULL on error.
+ *
+ * \since This function is available since SDL_image 3.4.0.
+ *
+ * \sa IMG_LoadGPUTexture
+ * \sa IMG_LoadGPUTextureTyped_IO
+ */
+extern SDL_DECLSPEC SDL_GPUTexture * SDLCALL IMG_LoadGPUTexture_IO(SDL_GPUDevice *device, SDL_GPUCopyPass *copy_pass, SDL_IOStream *src, bool closeio, int *width, int *height);
+
+/**
+ * Load an image from an SDL data source into a GPU texture.
+ *
+ * An SDL_GPUTexture represents an image in GPU memory, usable by SDL's GPU
+ * API. Regardless of the source format of the image, this function will create a
+ * GPU texture with the format SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM with no mip
+ * levels. It can be bound as a sampled texture from a graphics or compute pipeline
+ * and as a a readonly storage texture in a compute pipeline.
+ *
+ * If `closeio` is true, `src` will be closed before returning, whether this
+ * function succeeds or not. SDL_image reads everything it needs from `src`
+ * during this call in any case.
+ *
+ * Even though this function accepts a file type, SDL_image may still try
+ * other decoders that are capable of detecting file type from the contents of
+ * the image data, but may rely on the caller-provided type string for formats
+ * that it cannot autodetect. If `type` is NULL, SDL_image will rely solely on
+ * its ability to guess the format.
+ *
+ * There is a separate function to read files from disk without having to deal
+ * with SDL_IOStream: `IMG_LoadGPUTexture(device, copy_pass, "filename.jpg", width, height)
+ * will call this function and manage those details for you, determining the file type
+ * from the filename's extension.
+ *
+ * There is also IMG_LoadGPUTexture_IO(), which is equivalent to this function
+ * except that it will rely on SDL_image to determine what type of data it is
+ * loading, much like passing a NULL for type.
+ *
+ * When done with the returned texture, the app should dispose of it with a
+ * call to SDL_ReleaseGPUTexture().
+ *
+ * \param device the SDL_GPUDevice to use to create the GPU texture.
+ * \param copy_pass the SDL_GPUCopyPass to use to upload the loaded image to
+ *                  the GPU texture.
+ * \param src an SDL_IOStream that data will be read from.
+ * \param closeio true to close/free the SDL_IOStream before returning, false
+ *                to leave it open.
+ * \param type a filename extension that represent this data ("BMP", "GIF",
+ *             "PNG", etc).
+ * \param width a pointer filled in with the width of the GPU texture. may be NULL.
+ * \param height a pointer filled in with the width of the GPU texture. may be NULL.
+ * \returns a new GPU texture, or NULL on error.
+ *
+ * \since This function is available since SDL_image 3.4.0.
+ *
+ * \sa IMG_LoadGPUTexture
+ * \sa IMG_LoadGPUTexture_IO
+ */
+extern SDL_DECLSPEC SDL_GPUTexture * SDLCALL IMG_LoadGPUTextureTyped_IO(SDL_GPUDevice *device, SDL_GPUCopyPass *copy_pass, SDL_IOStream *src, bool closeio, const char *type, int *width, int *height);
+
 /**
  * Get the image currently in the clipboard.
  *
diff --git a/src/IMG_gpu.c b/src/IMG_gpu.c
new file mode 100644
index 00000000..e002b0fd
--- /dev/null
+++ b/src/IMG_gpu.c
@@ -0,0 +1,141 @@
+/*
+  SDL_image:  An example image loading library for use with SDL
+  Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+
+#include <SDL3_image/SDL_image.h>
+
+static SDL_GPUTexture * LoadGPUTexture(SDL_GPUDevice *device, SDL_GPUCopyPass *copy_pass, SDL_Surface *surface, Uint32 *width, Uint32 *height)
+{
+    if (width) {
+        *width = 0;
+    }
+    if (height) {
+        *height = 0;
+    }
+    if (!surface) {
+        return NULL;
+    }
+
+    if (surface->format != SDL_PIXELFORMAT_RGBA32) {
+        SDL_Surface *old_surface = surface;
+        surface = SDL_ConvertSurface(old_surface, SDL_PIXELFORMAT_RGBA32);
+        SDL_DestroySurface(old_surface);
+        if (!surface) {
+            return NULL;
+        }
+    }
+
+    SDL_GPUTextureCreateInfo texture_create_info = {0};
+    texture_create_info.format = SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM;
+    texture_create_info.type = SDL_GPU_TEXTURETYPE_2D;
+    texture_create_info.layer_count_or_depth = 1;
+    texture_create_info.num_levels = 1;
+    texture_create_info.usage = SDL_GPU_TEXTUREUSAGE_SAMPLER | SDL_GPU_TEXTUREUSAGE_COMPUTE_STORAGE_READ;
+    texture_create_info.width = surface->w;
+    texture_create_info.height = surface->h;
+    SDL_GPUTexture  *texture = SDL_CreateGPUTexture(device, &texture_create_info);
+    if (!texture) {
+        SDL_DestroySurface(surface);
+        return NULL;
+    }
+
+    SDL_GPUTransferBufferCreateInfo transfer_buffer_create_info = {0};
+    transfer_buffer_create_info.size = surface->w * surface->h * 4;
+    transfer_buffer_create_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
+    SDL_GPUTransferBuffer *transfer_buffer = SDL_CreateGPUTransferBuffer(device, &transfer_buffer_create_info);
+    if (!transfer_buffer) {
+        SDL_DestroySurface(surface);
+        SDL_ReleaseGPUTexture(device, texture);
+        return NULL;
+    }
+
+    Uint8 *dst = SDL_MapGPUTransferBuffer(device, transfer_buffer, 0);
+    if (!dst) {
+        SDL_DestroySurface(surface);
+        SDL_ReleaseGPUTexture(device, texture);
+        SDL_ReleaseGPUTransferBuffer(device, transfer_buffer);
+        return NULL;
+    }
+    const Uint8 *src = surface->pixels;
+    const int row_bytes = surface->w * 4;
+    if (row_bytes == surface->pitch) {
+        SDL_memcpy(dst, src, row_bytes * surface->h);
+    }
+    else {
+        for (int y = 0; y < surface->h; y++) {
+            SDL_memcpy(dst + y * row_bytes, src + y * surface->pitch, row_bytes);
+        }
+    }
+    SDL_UnmapGPUTransferBuffer(device, transfer_buffer);
+
+    SDL_GPUTextureTransferInfo texture_transfer_info = {0};
+    SDL_GPUTextureRegion texture_region = {0};
+    texture_transfer_info.transfer_buffer = transfer_buffer;
+    texture_region.texture = texture;
+    texture_region.w = surface->w;
+    texture_region.h = surface->h;
+    texture_region.d = 1;
+    SDL_UploadToGPUTexture(copy_pass, &texture_transfer_info, &texture_region, 0);
+
+    if (width) {
+        *width = surface->w;
+    }
+    if (height) {
+        *height = surface->h;
+    }
+
+    SDL_DestroySurface(surface);
+    SDL_ReleaseGPUTransferBuffer(device, transfer_buffer);
+
+    return texture;
+}
+
+SDL_GPUTexture * IMG_LoadGPUTexture(SDL_GPUDevice *device, SDL_GPUCopyPass *copy_pass, const char *file, int *width, int *height)
+{
+    if (!device) {
+        SDL_InvalidParamError("device");
+        return NULL;
+    }
+    if (!copy_pass) {
+        SDL_InvalidParamError("copy_pass");
+        return NULL;
+    }
+
+    return LoadGPUTexture(device, copy_pass, IMG_Load(file), width, height);
+}
+
+SDL_GPUTexture * IMG_LoadGPUTexture_IO(SDL_GPUDevice *device, SDL_GPUCopyPass *copy_pass, SDL_IOStream *src, bool closeio, int *width, int *height)
+{
+    return IMG_LoadGPUTextureTyped_IO(device, copy_pass, src, closeio, NULL, width, height);
+}
+
+SDL_GPUTexture * IMG_LoadGPUTextureTyped_IO(SDL_GPUDevice *device, SDL_GPUCopyPass *copy_pass, SDL_IOStream *src, bool closeio, const char *type, int *width, int *height)
+{
+    if (!device) {
+        SDL_InvalidParamError("device");
+        return NULL;
+    }
+    if (!copy_pass) {
+        SDL_InvalidParamError("copy_pass");
+        return NULL;
+    }
+
+    return LoadGPUTexture(device, copy_pass, IMG_LoadTyped_IO(src, closeio, type), width, height);
+}
diff --git a/src/SDL_image.exports b/src/SDL_image.exports
index da3336a4..ec45da5c 100644
--- a/src/SDL_image.exports
+++ b/src/SDL_image.exports
@@ -98,4 +98,7 @@ _IMG_SaveAPNGAnimation_IO
 _IMG_SaveAVIFAnimation_IO
 _IMG_SaveGIFAnimation_IO
 _IMG_SaveWEBPAnimation_IO
+_IMG_LoadGPUTexture
+_IMG_LoadGPUTexture_IO
+_IMG_LoadGPUTextureTyped_IO
 # extra symbols go here (don't modify this line)
diff --git a/src/SDL_image.sym b/src/SDL_image.sym
index b6215801..251e8c67 100644
--- a/src/SDL_image.sym
+++ b/src/SDL_image.sym
@@ -99,6 +99,9 @@ SDL3_image_0.0.0 {
     IMG_SaveAVIFAnimation_IO;
     IMG_SaveGIFAnimation_IO;
     IMG_SaveWEBPAnimation_IO;
+    IMG_LoadGPUTexture;
+    IMG_LoadGPUTexture_IO;
+    IMG_LoadGPUTextureTyped_IO;
     # extra symbols go here (don't modify this line)
   local: *;
 };