Browse Source

Added support for textures with palettes

Closes https://github.com/libsdl-org/SDL/pull/6192
Sam Lantinga 1 week ago
parent
commit
0b4b254a53

+ 39 - 1
include/SDL3/SDL_render.h

@@ -660,6 +660,7 @@ extern SDL_DECLSPEC SDL_Texture * SDLCALL SDL_CreateTextureFromSurface(SDL_Rende
  *   pixels, required
  * - `SDL_PROP_TEXTURE_CREATE_HEIGHT_NUMBER`: the height of the texture in
  *   pixels, required
+ * - `SDL_PROP_TEXTURE_CREATE_PALETTE_POINTER`: an SDL_Palette to use with palettized texture formats. This can be set later with SDL_SetTexturePalette()
  * - `SDL_PROP_TEXTURE_CREATE_SDR_WHITE_POINT_FLOAT`: for HDR10 and floating
  *   point textures, this defines the value of 100% diffuse white, with higher
  *   values being displayed in the High Dynamic Range headroom. This defaults
@@ -759,6 +760,7 @@ extern SDL_DECLSPEC SDL_Texture * SDLCALL SDL_CreateTextureWithProperties(SDL_Re
 #define SDL_PROP_TEXTURE_CREATE_ACCESS_NUMBER               "SDL.texture.create.access"
 #define SDL_PROP_TEXTURE_CREATE_WIDTH_NUMBER                "SDL.texture.create.width"
 #define SDL_PROP_TEXTURE_CREATE_HEIGHT_NUMBER               "SDL.texture.create.height"
+#define SDL_PROP_TEXTURE_CREATE_PALETTE_POINTER             "SDL.texture.create.palette"
 #define SDL_PROP_TEXTURE_CREATE_SDR_WHITE_POINT_FLOAT       "SDL.texture.create.SDR_white_point"
 #define SDL_PROP_TEXTURE_CREATE_HDR_HEADROOM_FLOAT          "SDL.texture.create.HDR_headroom"
 #define SDL_PROP_TEXTURE_CREATE_D3D11_TEXTURE_POINTER       "SDL.texture.create.d3d11.texture"
@@ -929,6 +931,42 @@ extern SDL_DECLSPEC SDL_Renderer * SDLCALL SDL_GetRendererFromTexture(SDL_Textur
  */
 extern SDL_DECLSPEC bool SDLCALL SDL_GetTextureSize(SDL_Texture *texture, float *w, float *h);
 
+/**
+ * Set the palette used by a texture.
+ *
+ * Setting the palette keeps an internal reference to the palette, which can be safely destroyed afterwards.
+ *
+ * A single palette can be shared with many textures.
+ *
+ * \param texture the texture to update.
+ * \param palette the SDL_Palette structure to use.
+ * \returns true on success or false on failure; 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_CreatePalette
+ * \sa SDL_GetTexturePalette
+ */
+extern SDL_DECLSPEC bool SDLCALL SDL_SetTexturePalette(SDL_Texture *texture, SDL_Palette *palette);
+
+/**
+ * Get the palette used by a texture.
+ *
+ * \param texture the texture to query.
+ * \returns a pointer to the palette used by the texture, or NULL if there is
+ *          no palette used.
+ *
+ * \threadsafety This function should only be called on the main thread.
+ *
+ * \since This function is available since SDL 3.4.0.
+ *
+ * \sa SDL_SetTexturePalette
+ */
+extern SDL_DECLSPEC SDL_Palette * SDLCALL SDL_GetTexturePalette(SDL_Texture *texture);
+
 /**
  * Set an additional color value multiplied into render copy operations.
  *
@@ -1157,7 +1195,7 @@ extern SDL_DECLSPEC bool SDLCALL SDL_GetTextureBlendMode(SDL_Texture *texture, S
  *
  * The default texture scale mode is SDL_SCALEMODE_LINEAR.
  *
- * If the scale mode is not supported, the closest supported mode is chosen.
+ * If the scale mode is not supported, the closest supported mode is chosen. Palettized textures will use SDL_SCALEMODE_PIXELART instead of SDL_SCALEMODE_LINEAR.
  *
  * \param texture the texture to update.
  * \param scaleMode the SDL_ScaleMode to use for texture scaling.

+ 2 - 0
include/SDL3/SDL_surface.h

@@ -327,6 +327,8 @@ extern SDL_DECLSPEC SDL_Palette * SDLCALL SDL_CreateSurfacePalette(SDL_Surface *
 /**
  * Set the palette used by a surface.
  *
+ * Setting the palette keeps an internal reference to the palette, which can be safely destroyed afterwards.
+ *
  * A single palette can be shared with many surfaces.
  *
  * \param surface the SDL_Surface structure to update.

+ 2 - 0
src/dynapi/SDL_dynapi.sym

@@ -1257,6 +1257,8 @@ SDL3_0.0.0 {
     SDL_hid_get_properties;
     SDL_GetPixelFormatFromGPUTextureFormat;
     SDL_GetGPUTextureFormatFromPixelFormat;
+    SDL_SetTexturePalette;
+    SDL_GetTexturePalette;
     # extra symbols go here (don't modify this line)
   local: *;
 };

+ 2 - 0
src/dynapi/SDL_dynapi_overrides.h

@@ -1283,3 +1283,5 @@
 #define SDL_GetPixelFormatFromGPUTextureFormat SDL_GetPixelFormatFromGPUTextureFormat_REAL
 #define SDL_GetGPUTextureFormatFromPixelFormat SDL_GetGPUTextureFormatFromPixelFormat_REAL
 #define JNI_OnLoad JNI_OnLoad_REAL
+#define SDL_SetTexturePalette SDL_SetTexturePalette_REAL
+#define SDL_GetTexturePalette SDL_GetTexturePalette_REAL

+ 2 - 0
src/dynapi/SDL_dynapi_procs.h

@@ -1291,3 +1291,5 @@ SDL_DYNAPI_PROC(SDL_PropertiesID,SDL_hid_get_properties,(SDL_hid_device *a),(a),
 SDL_DYNAPI_PROC(SDL_PixelFormat,SDL_GetPixelFormatFromGPUTextureFormat,(SDL_GPUTextureFormat a),(a),return)
 SDL_DYNAPI_PROC(SDL_GPUTextureFormat,SDL_GetGPUTextureFormatFromPixelFormat,(SDL_PixelFormat a),(a),return)
 SDL_DYNAPI_PROC(Sint32,JNI_OnLoad,(JavaVM *a, void *b),(a,b),return)
+SDL_DYNAPI_PROC(bool,SDL_SetTexturePalette,(SDL_Texture *a,SDL_Palette *b),(a,b),return)
+SDL_DYNAPI_PROC(SDL_Palette*,SDL_GetTexturePalette,(SDL_Texture *a),(a),return)

+ 206 - 70
src/render/SDL_render.c

@@ -697,6 +697,46 @@ static bool QueueCmdFillRects(SDL_Renderer *renderer, const SDL_FRect *rects, co
     return result;
 }
 
+static bool UpdateTexturePalette(SDL_Texture *texture)
+{
+    SDL_Renderer *renderer = texture->renderer;
+
+    if (!SDL_ISPIXELFORMAT_INDEXED(texture->format)) {
+        return true;
+    }
+
+    if (!texture->palette) {
+        return SDL_SetError("Texture doesn't have a palette");
+    }
+
+    if (texture->palette_version == texture->palette->version) {
+        return true;
+    }
+
+    if (texture->native) {
+        if (!FlushRenderCommandsIfTextureNeeded(texture->native)) {
+            return false;
+        }
+
+        SDL_Surface *surface;
+        bool result = SDL_LockTextureToSurface(texture->native, NULL, &surface);
+        if (result) {
+            result = SDL_BlitSurface(texture->palette_surface, NULL, surface, NULL);
+            SDL_UnlockTexture(texture->native);
+        }
+        if (!result) {
+            return false;
+        }
+    } else {
+        if (!renderer->UpdateTexturePalette(renderer, texture)) {
+            return false;
+        }
+    }
+
+    texture->palette_version = texture->palette->version;
+    return true;
+}
+
 static bool QueueCmdCopy(SDL_Renderer *renderer, SDL_Texture *texture, const SDL_FRect *srcrect, const SDL_FRect *dstrect)
 {
     SDL_RenderCommand *cmd = PrepQueueCmdDraw(renderer, SDL_RENDERCMD_COPY, texture);
@@ -1408,6 +1448,7 @@ SDL_Texture *SDL_CreateTextureWithProperties(SDL_Renderer *renderer, SDL_Propert
     SDL_TextureAccess access = (SDL_TextureAccess)SDL_GetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_ACCESS_NUMBER, SDL_TEXTUREACCESS_STATIC);
     int w = (int)SDL_GetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_WIDTH_NUMBER, 0);
     int h = (int)SDL_GetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_HEIGHT_NUMBER, 0);
+    SDL_Palette *palette = (SDL_Palette *)SDL_GetPointerProperty(props, SDL_PROP_TEXTURE_CREATE_PALETTE_POINTER, NULL);
     SDL_Colorspace default_colorspace;
     bool texture_is_fourcc_and_target;
 
@@ -1421,8 +1462,8 @@ SDL_Texture *SDL_CreateTextureWithProperties(SDL_Renderer *renderer, SDL_Propert
         SDL_SetError("Invalid texture format");
         return NULL;
     }
-    CHECK_PARAM(SDL_ISPIXELFORMAT_INDEXED(format) && !IsSupportedFormat(renderer, format)) {
-        SDL_SetError("Palettized textures are not supported");
+    CHECK_PARAM(SDL_ISPIXELFORMAT_INDEXED(format) && access == SDL_TEXTUREACCESS_TARGET) {
+        SDL_SetError("Palettized textures can't be render targets");
         return NULL;
     }
     CHECK_PARAM(w <= 0 || h <= 0) {
@@ -1453,7 +1494,12 @@ SDL_Texture *SDL_CreateTextureWithProperties(SDL_Renderer *renderer, SDL_Propert
     texture->color.b = 1.0f;
     texture->color.a = 1.0f;
     texture->blendMode = SDL_ISPIXELFORMAT_ALPHA(format) ? SDL_BLENDMODE_BLEND : SDL_BLENDMODE_NONE;
-    texture->scaleMode = renderer->scale_mode;
+    if (renderer->scale_mode == SDL_SCALEMODE_LINEAR &&
+        SDL_ISPIXELFORMAT_INDEXED(format)) {
+        texture->scaleMode = SDL_SCALEMODE_PIXELART;
+    } else {
+        texture->scaleMode = renderer->scale_mode;
+    }
     texture->view.pixel_w = w;
     texture->view.pixel_h = h;
     texture->view.viewport.w = -1;
@@ -1506,7 +1552,12 @@ SDL_Texture *SDL_CreateTextureWithProperties(SDL_Renderer *renderer, SDL_Propert
             }
         }
         SDL_SetNumberProperty(native_props, SDL_PROP_TEXTURE_CREATE_FORMAT_NUMBER, closest_format);
-        SDL_SetNumberProperty(native_props, SDL_PROP_TEXTURE_CREATE_ACCESS_NUMBER, texture->access);
+        if (SDL_ISPIXELFORMAT_INDEXED(texture->format)) {
+            // We're going to be uploading pixels frequently as the palette changes
+            SDL_SetNumberProperty(native_props, SDL_PROP_TEXTURE_CREATE_ACCESS_NUMBER, SDL_TEXTUREACCESS_STREAMING);
+        } else {
+            SDL_SetNumberProperty(native_props, SDL_PROP_TEXTURE_CREATE_ACCESS_NUMBER, texture->access);
+        }
         SDL_SetNumberProperty(native_props, SDL_PROP_TEXTURE_CREATE_WIDTH_NUMBER, texture->w);
         SDL_SetNumberProperty(native_props, SDL_PROP_TEXTURE_CREATE_HEIGHT_NUMBER, texture->h);
 
@@ -1532,6 +1583,8 @@ SDL_Texture *SDL_CreateTextureWithProperties(SDL_Renderer *renderer, SDL_Propert
         texture->next = texture->native;
         renderer->textures = texture;
 
+        SDL_SetTextureScaleMode(texture->native, texture->scaleMode);
+
         if (texture->format == SDL_PIXELFORMAT_MJPG) {
             // We have a custom decode + upload path for this
         } else if (SDL_ISPIXELFORMAT_FOURCC(texture->format)) {
@@ -1544,6 +1597,12 @@ SDL_Texture *SDL_CreateTextureWithProperties(SDL_Renderer *renderer, SDL_Propert
                 SDL_DestroyTexture(texture);
                 return NULL;
             }
+        } else if (SDL_ISPIXELFORMAT_INDEXED(texture->format)) {
+            texture->palette_surface = SDL_CreateSurface(w, h, texture->format);
+            if (!texture->palette_surface) {
+                SDL_DestroyTexture(texture);
+                return NULL;
+            }
         } else if (access == SDL_TEXTUREACCESS_STREAMING) {
             // The pitch is 4 byte aligned
             texture->pitch = (((w * SDL_BYTESPERPIXEL(format)) + 3) & ~3);
@@ -1555,6 +1614,10 @@ SDL_Texture *SDL_CreateTextureWithProperties(SDL_Renderer *renderer, SDL_Propert
         }
     }
 
+    if (SDL_ISPIXELFORMAT_INDEXED(texture->format) && palette) {
+        SDL_SetTexturePalette(texture, palette);
+    }
+
     // Now set the properties for the new texture
     props = SDL_GetTextureProperties(texture);
     SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_COLORSPACE_NUMBER, texture->colorspace);
@@ -1584,50 +1647,11 @@ SDL_Texture *SDL_CreateTexture(SDL_Renderer *renderer, SDL_PixelFormat format, S
 
 static bool SDL_UpdateTextureFromSurface(SDL_Texture *texture, SDL_Rect *rect, SDL_Surface *surface)
 {
-    SDL_TextureAccess access;
     bool direct_update;
-    SDL_PixelFormat tex_format;
-    SDL_PropertiesID surface_props;
-    SDL_PropertiesID tex_props;
-    SDL_Colorspace surface_colorspace = SDL_COLORSPACE_UNKNOWN;
-    SDL_Colorspace texture_colorspace = SDL_COLORSPACE_UNKNOWN;
-
-    if (texture == NULL || surface == NULL) {
-        return false;
-    }
-
-    tex_props = SDL_GetTextureProperties(texture);
-    if (!tex_props) {
-        return false;
-    }
-
-    surface_props = SDL_GetSurfaceProperties(surface);
-    if (!surface_props) {
-        return false;
-    }
-
-    tex_format = (SDL_PixelFormat)SDL_GetNumberProperty(tex_props, SDL_PROP_TEXTURE_FORMAT_NUMBER, 0);
-    access = (SDL_TextureAccess)SDL_GetNumberProperty(tex_props, SDL_PROP_TEXTURE_ACCESS_NUMBER, 0);
 
-    if (access != SDL_TEXTUREACCESS_STATIC && access != SDL_TEXTUREACCESS_STREAMING) {
-        return false;
-    }
-
-    surface_colorspace = SDL_GetSurfaceColorspace(surface);
-    texture_colorspace = surface_colorspace;
-
-    if (surface_colorspace == SDL_COLORSPACE_SRGB_LINEAR ||
-        SDL_COLORSPACETRANSFER(surface_colorspace) == SDL_TRANSFER_CHARACTERISTICS_PQ) {
-        if (SDL_ISPIXELFORMAT_FLOAT(tex_format)) {
-            texture_colorspace = SDL_COLORSPACE_SRGB_LINEAR;
-        } else if (SDL_ISPIXELFORMAT_10BIT(tex_format)) {
-            texture_colorspace = SDL_COLORSPACE_HDR10;
-        } else {
-            texture_colorspace = SDL_COLORSPACE_SRGB;
-        }
-    }
-
-    if (tex_format == surface->format && texture_colorspace == surface_colorspace) {
+    if (surface->format == texture->format &&
+        surface->palette == texture->palette &&
+        SDL_GetSurfaceColorspace(surface) == texture->colorspace) {
         if (SDL_ISPIXELFORMAT_ALPHA(surface->format) && SDL_SurfaceHasColorKey(surface)) {
             /* Surface and Renderer formats are identical.
              * Intermediate conversion is needed to convert color key to alpha (SDL_ConvertColorkeyToAlpha()). */
@@ -1643,17 +1667,16 @@ static bool SDL_UpdateTextureFromSurface(SDL_Texture *texture, SDL_Rect *rect, S
 
     if (direct_update) {
         if (SDL_MUSTLOCK(surface)) {
-            SDL_LockSurface(surface);
-            SDL_UpdateTexture(texture, rect, surface->pixels, surface->pitch);
-            SDL_UnlockSurface(surface);
+            if (SDL_LockSurface(surface)) {
+                SDL_UpdateTexture(texture, rect, surface->pixels, surface->pitch);
+                SDL_UnlockSurface(surface);
+            }
         } else {
             SDL_UpdateTexture(texture, rect, surface->pixels, surface->pitch);
         }
     } else {
-        SDL_Surface *temp = NULL;
-
         // Set up a destination surface for the texture update
-        temp = SDL_ConvertSurfaceAndColorspace(surface, tex_format, NULL, texture_colorspace, surface_props);
+        SDL_Surface *temp = SDL_ConvertSurfaceAndColorspace(surface, texture->format, texture->palette, texture->colorspace, SDL_GetSurfaceProperties(surface));
         if (temp) {
             SDL_UpdateTexture(texture, NULL, temp->pixels, temp->pitch);
             SDL_DestroySurface(temp);
@@ -1709,7 +1732,7 @@ SDL_Texture *SDL_CreateTextureFromSurface(SDL_Renderer *renderer, SDL_Surface *s
         needAlpha = false;
     }
 
-    // If Palette contains alpha values, promotes to alpha format
+    // If palette contains alpha values, promotes to alpha format
     palette = SDL_GetSurfacePalette(surface);
     if (palette) {
         bool is_opaque, has_alpha_channel;
@@ -1806,6 +1829,9 @@ SDL_Texture *SDL_CreateTextureFromSurface(SDL_Renderer *renderer, SDL_Surface *s
     SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_ACCESS_NUMBER, SDL_TEXTUREACCESS_STATIC);
     SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_WIDTH_NUMBER, surface->w);
     SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_HEIGHT_NUMBER, surface->h);
+    if (format == surface->format && palette) {
+        SDL_SetPointerProperty(props, SDL_PROP_TEXTURE_CREATE_PALETTE_POINTER, palette);
+    }
     texture = SDL_CreateTextureWithProperties(renderer, props);
     SDL_DestroyProperties(props);
     if (!texture) {
@@ -1857,6 +1883,44 @@ bool SDL_GetTextureSize(SDL_Texture *texture, float *w, float *h)
     return true;
 }
 
+bool SDL_SetTexturePalette(SDL_Texture *texture, SDL_Palette *palette)
+{
+    CHECK_TEXTURE_MAGIC(texture, false);
+
+    CHECK_PARAM(!SDL_ISPIXELFORMAT_INDEXED(texture->format)) {
+        return SDL_SetError("Texture isn't palettized format");
+    }
+
+    CHECK_PARAM(palette && palette->ncolors > (1 << SDL_BITSPERPIXEL(texture->format))) {
+        return SDL_SetError("SDL_SetSurfacePalette() passed a palette that doesn't match the surface format");
+    }
+
+    if (palette != texture->palette) {
+        if (texture->palette) {
+            SDL_DestroyPalette(texture->palette);
+        }
+
+        texture->palette = palette;
+        texture->palette_version = 0;
+
+        if (texture->palette) {
+            ++texture->palette->refcount;
+        }
+
+        if (texture->palette_surface) {
+            SDL_SetSurfacePalette(texture->palette_surface, palette);
+        }
+    }
+    return true;
+}
+
+SDL_Palette *SDL_GetTexturePalette(SDL_Texture *texture)
+{
+    CHECK_TEXTURE_MAGIC(texture, NULL);
+
+    return texture->palette;
+}
+
 bool SDL_SetTextureColorMod(SDL_Texture *texture, Uint8 r, Uint8 g, Uint8 b)
 {
     const float fR = (float)r / 255.0f;
@@ -2028,9 +2092,13 @@ bool SDL_SetTextureScaleMode(SDL_Texture *texture, SDL_ScaleMode scaleMode)
 
     switch (scaleMode) {
     case SDL_SCALEMODE_NEAREST:
-    case SDL_SCALEMODE_LINEAR:
     case SDL_SCALEMODE_PIXELART:
         break;
+    case SDL_SCALEMODE_LINEAR:
+        if (SDL_ISPIXELFORMAT_INDEXED(texture->format)) {
+            scaleMode = SDL_SCALEMODE_PIXELART;
+        }
+        break;
     default:
         return SDL_InvalidParamError("scaleMode");
     }
@@ -2046,7 +2114,7 @@ bool SDL_SetTextureScaleMode(SDL_Texture *texture, SDL_ScaleMode scaleMode)
 bool SDL_GetTextureScaleMode(SDL_Texture *texture, SDL_ScaleMode *scaleMode)
 {
     if (scaleMode) {
-        *scaleMode = SDL_SCALEMODE_LINEAR;
+        *scaleMode = SDL_SCALEMODE_INVALID;
     }
 
     CHECK_TEXTURE_MAGIC(texture, false);
@@ -2105,14 +2173,26 @@ static bool SDL_UpdateTextureYUV(SDL_Texture *texture, const SDL_Rect *rect,
 }
 #endif // SDL_HAVE_YUV
 
-static bool SDL_UpdateTextureNative(SDL_Texture *texture, const SDL_Rect *rect,
-                                   const void *pixels, int pitch)
+static bool SDL_UpdateTexturePaletteSurface(SDL_Texture *texture, const SDL_Rect *rect, const void *pixels, int pitch)
 {
-    SDL_Texture *native = texture->native;
+    SDL_Surface *surface = texture->palette_surface;
 
-    if (!rect->w || !rect->h) {
-        return true; // nothing to do.
+    const Uint8 *src = (const Uint8 *)pixels;
+    Uint8 *dst = ((Uint8 *)surface->pixels) + rect->y * surface->pitch + (rect->x * SDL_BITSPERPIXEL(texture->format)) / 8;
+    int w = ((rect->w * SDL_BITSPERPIXEL(texture->format)) + 7) / 8;
+    int h = rect->h;
+    while (h--) {
+        SDL_memcpy(dst, src, w);
+        src += pitch;
+        dst += surface->pitch;
     }
+    texture->palette_version = 0;
+    return true;
+}
+
+static bool SDL_UpdateTextureNative(SDL_Texture *texture, const SDL_Rect *rect, const void *pixels, int pitch)
+{
+    SDL_Texture *native = texture->native;
 
     if (texture->access == SDL_TEXTUREACCESS_STREAMING) {
         // We can lock the texture and copy to it
@@ -2174,6 +2254,8 @@ bool SDL_UpdateTexture(SDL_Texture *texture, const SDL_Rect *rect, const void *p
     } else if (texture->yuv) {
         return SDL_UpdateTextureYUV(texture, &real_rect, pixels, pitch);
 #endif
+    } else if (texture->palette_surface) {
+        return SDL_UpdateTexturePaletteSurface(texture, &real_rect, pixels, pitch);
     } else if (texture->native) {
         return SDL_UpdateTextureNative(texture, &real_rect, pixels, pitch);
     } else {
@@ -2426,8 +2508,16 @@ static bool SDL_LockTextureYUV(SDL_Texture *texture, const SDL_Rect *rect,
 }
 #endif // SDL_HAVE_YUV
 
-static bool SDL_LockTextureNative(SDL_Texture *texture, const SDL_Rect *rect,
-                                 void **pixels, int *pitch)
+static bool SDL_LockTexturePaletteSurface(SDL_Texture *texture, const SDL_Rect *rect, void **pixels, int *pitch)
+{
+    SDL_Surface *surface = texture->palette_surface;
+
+    *pixels = ((Uint8 *)surface->pixels) + rect->y * surface->pitch + (rect->x * SDL_BITSPERPIXEL(texture->format)) / 8;
+    *pitch = surface->pitch;
+    return true;
+}
+
+static bool SDL_LockTextureNative(SDL_Texture *texture, const SDL_Rect *rect, void **pixels, int *pitch)
 {
     texture->locked_rect = *rect;
     *pixels = (void *)((Uint8 *)texture->pixels +
@@ -2463,7 +2553,9 @@ bool SDL_LockTexture(SDL_Texture *texture, const SDL_Rect *rect, void **pixels,
         return SDL_LockTextureYUV(texture, rect, pixels, pitch);
     } else
 #endif
-        if (texture->native) {
+    if (texture->palette_surface) {
+        return SDL_LockTexturePaletteSurface(texture, rect, pixels, pitch);
+    } else if (texture->native) {
         // Calls a real SDL_LockTexture/SDL_UnlockTexture on unlock, flushing then.
         return SDL_LockTextureNative(texture, rect, pixels, pitch);
     } else {
@@ -2504,6 +2596,9 @@ bool SDL_LockTextureToSurface(SDL_Texture *texture, const SDL_Rect *rect, SDL_Su
         SDL_UnlockTexture(texture);
         return false;
     }
+    if (texture->palette) {
+        SDL_SetSurfacePalette(texture->locked_surface, texture->palette);
+    }
 
     *surface = texture->locked_surface;
     return true;
@@ -2531,6 +2626,11 @@ static void SDL_UnlockTextureYUV(SDL_Texture *texture)
 }
 #endif // SDL_HAVE_YUV
 
+static void SDL_UnlockTexturePaletteSurface(SDL_Texture *texture)
+{
+    texture->palette_version = 0;
+}
+
 static void SDL_UnlockTextureNative(SDL_Texture *texture)
 {
     SDL_Texture *native = texture->native;
@@ -2564,15 +2664,19 @@ void SDL_UnlockTexture(SDL_Texture *texture)
         SDL_UnlockTextureYUV(texture);
     } else
 #endif
-        if (texture->native) {
+    if (texture->palette_surface) {
+        SDL_UnlockTexturePaletteSurface(texture);
+    } else if (texture->native) {
         SDL_UnlockTextureNative(texture);
     } else {
         SDL_Renderer *renderer = texture->renderer;
         renderer->UnlockTexture(renderer, texture);
     }
 
-    SDL_DestroySurface(texture->locked_surface);
-    texture->locked_surface = NULL;
+    if (texture->locked_surface) {
+        SDL_DestroySurface(texture->locked_surface);
+        texture->locked_surface = NULL;
+    }
 }
 
 bool SDL_SetRenderTarget(SDL_Renderer *renderer, SDL_Texture *texture)
@@ -3938,6 +4042,10 @@ bool SDL_RenderTexture(SDL_Renderer *renderer, SDL_Texture *texture, const SDL_F
         dstrect = &full_dstrect;
     }
 
+    if (!UpdateTexturePalette(texture)) {
+        return false;
+    }
+
     if (texture->native) {
         texture = texture->native;
     }
@@ -3983,6 +4091,10 @@ bool SDL_RenderTextureAffine(SDL_Renderer *renderer, SDL_Texture *texture,
 
     GetRenderViewportSize(renderer, &real_dstrect);
 
+    if (!UpdateTexturePalette(texture)) {
+        return false;
+    }
+
     if (texture->native) {
         texture = texture->native;
     }
@@ -4111,6 +4223,10 @@ bool SDL_RenderTextureRotated(SDL_Renderer *renderer, SDL_Texture *texture,
         dstrect = &full_dstrect;
     }
 
+    if (!UpdateTexturePalette(texture)) {
+        return false;
+    }
+
     if (texture->native) {
         texture = texture->native;
     }
@@ -4367,6 +4483,10 @@ bool SDL_RenderTextureTiled(SDL_Renderer *renderer, SDL_Texture *texture, const
         dstrect = &full_dstrect;
     }
 
+    if (!UpdateTexturePalette(texture)) {
+        return false;
+    }
+
     if (texture->native) {
         texture = texture->native;
     }
@@ -5137,8 +5257,14 @@ bool SDL_RenderGeometryRaw(SDL_Renderer *renderer,
         return true;
     }
 
-    if (texture && texture->native) {
-        texture = texture->native;
+    if (texture) {
+        if (!UpdateTexturePalette(texture)) {
+            return false;
+        }
+
+        if (texture->native) {
+            texture = texture->native;
+        }
     }
 
     texture_address_mode_u = renderer->texture_address_mode_u;
@@ -5381,6 +5507,10 @@ static void SDL_DestroyTextureInternal(SDL_Texture *texture, bool is_destroying)
 {
     SDL_Renderer *renderer;
 
+    if (texture->palette) {
+        SDL_SetTexturePalette(texture, NULL);
+    }
+
     SDL_DestroyProperties(texture->props);
 
     renderer = texture->renderer;
@@ -5417,8 +5547,14 @@ static void SDL_DestroyTextureInternal(SDL_Texture *texture, bool is_destroying)
 
     renderer->DestroyTexture(renderer, texture);
 
-    SDL_DestroySurface(texture->locked_surface);
-    texture->locked_surface = NULL;
+    if (texture->palette_surface) {
+        SDL_DestroySurface(texture->palette_surface);
+        texture->palette_surface = NULL;
+    }
+    if (texture->locked_surface) {
+        SDL_DestroySurface(texture->locked_surface);
+        texture->locked_surface = NULL;
+    }
 
     SDL_free(texture);
 }

+ 5 - 2
src/render/SDL_sysrender.h

@@ -81,6 +81,7 @@ struct SDL_Texture
     int refcount;               /**< Application reference count, used when freeing texture */
 
     // Private API definition
+    SDL_Renderer *renderer;
     SDL_Colorspace colorspace;  // The colorspace of the texture
     float SDR_white_point;      // The SDR white point for this content
     float HDR_headroom;         // The HDR headroom needed by this content
@@ -89,8 +90,9 @@ struct SDL_Texture
     SDL_ScaleMode scaleMode;    // The texture scale mode
     SDL_FColor color;           // Texture modulation values
     SDL_RenderViewState view;   // Target texture view state
-
-    SDL_Renderer *renderer;
+    SDL_Palette *palette;
+    Uint32 palette_version;
+    SDL_Surface *palette_surface;
 
     // Support for formats not supported directly by the renderer
     SDL_Texture *native;
@@ -233,6 +235,7 @@ struct SDL_Renderer
 
     void (*InvalidateCachedState)(SDL_Renderer *renderer);
     bool (*RunCommandQueue)(SDL_Renderer *renderer, SDL_RenderCommand *cmd, void *vertices, size_t vertsize);
+    bool (*UpdateTexturePalette)(SDL_Renderer *renderer, SDL_Texture *texture);
     bool (*UpdateTexture)(SDL_Renderer *renderer, SDL_Texture *texture,
                          const SDL_Rect *rect, const void *pixels,
                          int pitch);

+ 18 - 0
src/render/software/SDL_render_sw.c

@@ -126,6 +126,20 @@ static bool SW_CreateTexture(SDL_Renderer *renderer, SDL_Texture *texture, SDL_P
     return true;
 }
 
+static bool SW_UpdateTexturePalette(SDL_Renderer *renderer, SDL_Texture *texture)
+{
+    SDL_Surface *surface = (SDL_Surface *)texture->internal;
+    SDL_Palette *palette = texture->palette;
+
+    if (!surface->palette) {
+        surface->palette = SDL_CreatePalette(1 << SDL_BITSPERPIXEL(surface->format));
+        if (!surface->palette) {
+            return false;
+        }
+    }
+    return SDL_SetPaletteColors(surface->palette, palette->colors, 0, palette->ncolors);
+}
+
 static bool SW_UpdateTexture(SDL_Renderer *renderer, SDL_Texture *texture,
                             const SDL_Rect *rect, const void *pixels, int pitch)
 {
@@ -1115,6 +1129,9 @@ static void SW_SelectBestFormats(SDL_Renderer *renderer, SDL_PixelFormat format)
         SDL_AddSupportedTextureFormat(renderer, SDL_PIXELFORMAT_XRGB8888);
         SDL_AddSupportedTextureFormat(renderer, SDL_PIXELFORMAT_ARGB8888);
     }
+
+    // Add 8-bit palettized format
+    SDL_AddSupportedTextureFormat(renderer, SDL_PIXELFORMAT_INDEX8);
 }
 
 bool SW_CreateRendererForSurface(SDL_Renderer *renderer, SDL_Surface *surface, SDL_PropertiesID create_props)
@@ -1142,6 +1159,7 @@ bool SW_CreateRendererForSurface(SDL_Renderer *renderer, SDL_Surface *surface, S
     renderer->WindowEvent = SW_WindowEvent;
     renderer->GetOutputSize = SW_GetOutputSize;
     renderer->CreateTexture = SW_CreateTexture;
+    renderer->UpdateTexturePalette = SW_UpdateTexturePalette;
     renderer->UpdateTexture = SW_UpdateTexture;
     renderer->LockTexture = SW_LockTexture;
     renderer->UnlockTexture = SW_UnlockTexture;

+ 1 - 0
test/CMakeLists.txt

@@ -403,6 +403,7 @@ add_sdl_test_executable(testsoftwaretransparent SOURCES testsoftwaretransparent.
 add_sdl_test_executable(testsprite MAIN_CALLBACKS NEEDS_RESOURCES TESTUTILS SOURCES testsprite.c)
 add_sdl_test_executable(testspriteminimal SOURCES testspriteminimal.c ${icon_bmp_header} DEPENDS generate-icon_bmp_header)
 add_sdl_test_executable(testspritesurface SOURCES testspritesurface.c ${icon_bmp_header} DEPENDS generate-icon_bmp_header)
+add_sdl_test_executable(testpalette SOURCES testpalette.c)
 add_sdl_test_executable(teststreaming NEEDS_RESOURCES TESTUTILS SOURCES teststreaming.c)
 add_sdl_test_executable(testtimer NONINTERACTIVE NONINTERACTIVE_ARGS --no-interactive NONINTERACTIVE_TIMEOUT 60 SOURCES testtimer.c)
 add_sdl_test_executable(testurl SOURCES testurl.c)

+ 448 - 0
test/testpalette.c

@@ -0,0 +1,448 @@
+/*
+  Copyright (C) 1997-2025 Sam Lantinga <[email protected]>
+
+  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.
+*/
+/* Simple program:  Move N sprites around on the screen as fast as possible */
+
+#include <SDL3/SDL.h>
+#include <SDL3/SDL_main.h>
+
+#ifdef SDL_PLATFORM_EMSCRIPTEN
+#include <emscripten/emscripten.h>
+#endif
+
+#define WINDOW_WIDTH  640
+#define WINDOW_HEIGHT 480
+
+
+static const SDL_Color Palette[256] = {
+    { 255,   0,   0, SDL_ALPHA_OPAQUE },
+    { 255,   5,   0, SDL_ALPHA_OPAQUE },
+    { 255,  11,   0, SDL_ALPHA_OPAQUE },
+    { 255,  17,   0, SDL_ALPHA_OPAQUE },
+    { 255,  23,   0, SDL_ALPHA_OPAQUE },
+    { 255,  29,   0, SDL_ALPHA_OPAQUE },
+    { 255,  35,   0, SDL_ALPHA_OPAQUE },
+    { 255,  41,   0, SDL_ALPHA_OPAQUE },
+    { 255,  47,   0, SDL_ALPHA_OPAQUE },
+    { 255,  53,   0, SDL_ALPHA_OPAQUE },
+    { 255,  59,   0, SDL_ALPHA_OPAQUE },
+    { 255,  65,   0, SDL_ALPHA_OPAQUE },
+    { 255,  71,   0, SDL_ALPHA_OPAQUE },
+    { 255,  77,   0, SDL_ALPHA_OPAQUE },
+    { 255,  83,   0, SDL_ALPHA_OPAQUE },
+    { 255,  89,   0, SDL_ALPHA_OPAQUE },
+    { 255,  95,   0, SDL_ALPHA_OPAQUE },
+    { 255, 101,   0, SDL_ALPHA_OPAQUE },
+    { 255, 107,   0, SDL_ALPHA_OPAQUE },
+    { 255, 113,   0, SDL_ALPHA_OPAQUE },
+    { 255, 119,   0, SDL_ALPHA_OPAQUE },
+    { 255, 125,   0, SDL_ALPHA_OPAQUE },
+    { 255, 131,   0, SDL_ALPHA_OPAQUE },
+    { 255, 137,   0, SDL_ALPHA_OPAQUE },
+    { 255, 143,   0, SDL_ALPHA_OPAQUE },
+    { 255, 149,   0, SDL_ALPHA_OPAQUE },
+    { 255, 155,   0, SDL_ALPHA_OPAQUE },
+    { 255, 161,   0, SDL_ALPHA_OPAQUE },
+    { 255, 167,   0, SDL_ALPHA_OPAQUE },
+    { 255, 173,   0, SDL_ALPHA_OPAQUE },
+    { 255, 179,   0, SDL_ALPHA_OPAQUE },
+    { 255, 185,   0, SDL_ALPHA_OPAQUE },
+    { 255, 191,   0, SDL_ALPHA_OPAQUE },
+    { 255, 197,   0, SDL_ALPHA_OPAQUE },
+    { 255, 203,   0, SDL_ALPHA_OPAQUE },
+    { 255, 209,   0, SDL_ALPHA_OPAQUE },
+    { 255, 215,   0, SDL_ALPHA_OPAQUE },
+    { 255, 221,   0, SDL_ALPHA_OPAQUE },
+    { 255, 227,   0, SDL_ALPHA_OPAQUE },
+    { 255, 233,   0, SDL_ALPHA_OPAQUE },
+    { 255, 239,   0, SDL_ALPHA_OPAQUE },
+    { 255, 245,   0, SDL_ALPHA_OPAQUE },
+    { 255, 251,   0, SDL_ALPHA_OPAQUE },
+    { 253, 255,   0, SDL_ALPHA_OPAQUE },
+    { 247, 255,   0, SDL_ALPHA_OPAQUE },
+    { 241, 255,   0, SDL_ALPHA_OPAQUE },
+    { 235, 255,   0, SDL_ALPHA_OPAQUE },
+    { 229, 255,   0, SDL_ALPHA_OPAQUE },
+    { 223, 255,   0, SDL_ALPHA_OPAQUE },
+    { 217, 255,   0, SDL_ALPHA_OPAQUE },
+    { 211, 255,   0, SDL_ALPHA_OPAQUE },
+    { 205, 255,   0, SDL_ALPHA_OPAQUE },
+    { 199, 255,   0, SDL_ALPHA_OPAQUE },
+    { 193, 255,   0, SDL_ALPHA_OPAQUE },
+    { 187, 255,   0, SDL_ALPHA_OPAQUE },
+    { 181, 255,   0, SDL_ALPHA_OPAQUE },
+    { 175, 255,   0, SDL_ALPHA_OPAQUE },
+    { 169, 255,   0, SDL_ALPHA_OPAQUE },
+    { 163, 255,   0, SDL_ALPHA_OPAQUE },
+    { 157, 255,   0, SDL_ALPHA_OPAQUE },
+    { 151, 255,   0, SDL_ALPHA_OPAQUE },
+    { 145, 255,   0, SDL_ALPHA_OPAQUE },
+    { 139, 255,   0, SDL_ALPHA_OPAQUE },
+    { 133, 255,   0, SDL_ALPHA_OPAQUE },
+    { 127, 255,   0, SDL_ALPHA_OPAQUE },
+    { 121, 255,   0, SDL_ALPHA_OPAQUE },
+    { 115, 255,   0, SDL_ALPHA_OPAQUE },
+    { 109, 255,   0, SDL_ALPHA_OPAQUE },
+    { 103, 255,   0, SDL_ALPHA_OPAQUE },
+    {  97, 255,   0, SDL_ALPHA_OPAQUE },
+    {  91, 255,   0, SDL_ALPHA_OPAQUE },
+    {  85, 255,   0, SDL_ALPHA_OPAQUE },
+    {  79, 255,   0, SDL_ALPHA_OPAQUE },
+    {  73, 255,   0, SDL_ALPHA_OPAQUE },
+    {  67, 255,   0, SDL_ALPHA_OPAQUE },
+    {  61, 255,   0, SDL_ALPHA_OPAQUE },
+    {  55, 255,   0, SDL_ALPHA_OPAQUE },
+    {  49, 255,   0, SDL_ALPHA_OPAQUE },
+    {  43, 255,   0, SDL_ALPHA_OPAQUE },
+    {  37, 255,   0, SDL_ALPHA_OPAQUE },
+    {  31, 255,   0, SDL_ALPHA_OPAQUE },
+    {  25, 255,   0, SDL_ALPHA_OPAQUE },
+    {  19, 255,   0, SDL_ALPHA_OPAQUE },
+    {  13, 255,   0, SDL_ALPHA_OPAQUE },
+    {   7, 255,   0, SDL_ALPHA_OPAQUE },
+    {   1, 255,   0, SDL_ALPHA_OPAQUE },
+    {   0, 255,   3, SDL_ALPHA_OPAQUE },
+    {   0, 255,   9, SDL_ALPHA_OPAQUE },
+    {   0, 255,  15, SDL_ALPHA_OPAQUE },
+    {   0, 255,  21, SDL_ALPHA_OPAQUE },
+    {   0, 255,  27, SDL_ALPHA_OPAQUE },
+    {   0, 255,  33, SDL_ALPHA_OPAQUE },
+    {   0, 255,  39, SDL_ALPHA_OPAQUE },
+    {   0, 255,  45, SDL_ALPHA_OPAQUE },
+    {   0, 255,  51, SDL_ALPHA_OPAQUE },
+    {   0, 255,  57, SDL_ALPHA_OPAQUE },
+    {   0, 255,  63, SDL_ALPHA_OPAQUE },
+    {   0, 255,  69, SDL_ALPHA_OPAQUE },
+    {   0, 255,  75, SDL_ALPHA_OPAQUE },
+    {   0, 255,  81, SDL_ALPHA_OPAQUE },
+    {   0, 255,  87, SDL_ALPHA_OPAQUE },
+    {   0, 255,  93, SDL_ALPHA_OPAQUE },
+    {   0, 255,  99, SDL_ALPHA_OPAQUE },
+    {   0, 255, 105, SDL_ALPHA_OPAQUE },
+    {   0, 255, 111, SDL_ALPHA_OPAQUE },
+    {   0, 255, 117, SDL_ALPHA_OPAQUE },
+    {   0, 255, 123, SDL_ALPHA_OPAQUE },
+    {   0, 255, 129, SDL_ALPHA_OPAQUE },
+    {   0, 255, 135, SDL_ALPHA_OPAQUE },
+    {   0, 255, 141, SDL_ALPHA_OPAQUE },
+    {   0, 255, 147, SDL_ALPHA_OPAQUE },
+    {   0, 255, 153, SDL_ALPHA_OPAQUE },
+    {   0, 255, 159, SDL_ALPHA_OPAQUE },
+    {   0, 255, 165, SDL_ALPHA_OPAQUE },
+    {   0, 255, 171, SDL_ALPHA_OPAQUE },
+    {   0, 255, 177, SDL_ALPHA_OPAQUE },
+    {   0, 255, 183, SDL_ALPHA_OPAQUE },
+    {   0, 255, 189, SDL_ALPHA_OPAQUE },
+    {   0, 255, 195, SDL_ALPHA_OPAQUE },
+    {   0, 255, 201, SDL_ALPHA_OPAQUE },
+    {   0, 255, 207, SDL_ALPHA_OPAQUE },
+    {   0, 255, 213, SDL_ALPHA_OPAQUE },
+    {   0, 255, 219, SDL_ALPHA_OPAQUE },
+    {   0, 255, 225, SDL_ALPHA_OPAQUE },
+    {   0, 255, 231, SDL_ALPHA_OPAQUE },
+    {   0, 255, 237, SDL_ALPHA_OPAQUE },
+    {   0, 255, 243, SDL_ALPHA_OPAQUE },
+    {   0, 255, 249, SDL_ALPHA_OPAQUE },
+    {   0, 255, 255, SDL_ALPHA_OPAQUE },
+    {   0, 249, 255, SDL_ALPHA_OPAQUE },
+    {   0, 243, 255, SDL_ALPHA_OPAQUE },
+    {   0, 237, 255, SDL_ALPHA_OPAQUE },
+    {   0, 231, 255, SDL_ALPHA_OPAQUE },
+    {   0, 225, 255, SDL_ALPHA_OPAQUE },
+    {   0, 219, 255, SDL_ALPHA_OPAQUE },
+    {   0, 213, 255, SDL_ALPHA_OPAQUE },
+    {   0, 207, 255, SDL_ALPHA_OPAQUE },
+    {   0, 201, 255, SDL_ALPHA_OPAQUE },
+    {   0, 195, 255, SDL_ALPHA_OPAQUE },
+    {   0, 189, 255, SDL_ALPHA_OPAQUE },
+    {   0, 183, 255, SDL_ALPHA_OPAQUE },
+    {   0, 177, 255, SDL_ALPHA_OPAQUE },
+    {   0, 171, 255, SDL_ALPHA_OPAQUE },
+    {   0, 165, 255, SDL_ALPHA_OPAQUE },
+    {   0, 159, 255, SDL_ALPHA_OPAQUE },
+    {   0, 153, 255, SDL_ALPHA_OPAQUE },
+    {   0, 147, 255, SDL_ALPHA_OPAQUE },
+    {   0, 141, 255, SDL_ALPHA_OPAQUE },
+    {   0, 135, 255, SDL_ALPHA_OPAQUE },
+    {   0, 129, 255, SDL_ALPHA_OPAQUE },
+    {   0, 123, 255, SDL_ALPHA_OPAQUE },
+    {   0, 117, 255, SDL_ALPHA_OPAQUE },
+    {   0, 111, 255, SDL_ALPHA_OPAQUE },
+    {   0, 105, 255, SDL_ALPHA_OPAQUE },
+    {   0,  99, 255, SDL_ALPHA_OPAQUE },
+    {   0,  93, 255, SDL_ALPHA_OPAQUE },
+    {   0,  87, 255, SDL_ALPHA_OPAQUE },
+    {   0,  81, 255, SDL_ALPHA_OPAQUE },
+    {   0,  75, 255, SDL_ALPHA_OPAQUE },
+    {   0,  69, 255, SDL_ALPHA_OPAQUE },
+    {   0,  63, 255, SDL_ALPHA_OPAQUE },
+    {   0,  57, 255, SDL_ALPHA_OPAQUE },
+    {   0,  51, 255, SDL_ALPHA_OPAQUE },
+    {   0,  45, 255, SDL_ALPHA_OPAQUE },
+    {   0,  39, 255, SDL_ALPHA_OPAQUE },
+    {   0,  33, 255, SDL_ALPHA_OPAQUE },
+    {   0,  27, 255, SDL_ALPHA_OPAQUE },
+    {   0,  21, 255, SDL_ALPHA_OPAQUE },
+    {   0,  15, 255, SDL_ALPHA_OPAQUE },
+    {   0,   9, 255, SDL_ALPHA_OPAQUE },
+    {   0,   3, 255, SDL_ALPHA_OPAQUE },
+    {   1,   0, 255, SDL_ALPHA_OPAQUE },
+    {   7,   0, 255, SDL_ALPHA_OPAQUE },
+    {  13,   0, 255, SDL_ALPHA_OPAQUE },
+    {  19,   0, 255, SDL_ALPHA_OPAQUE },
+    {  25,   0, 255, SDL_ALPHA_OPAQUE },
+    {  31,   0, 255, SDL_ALPHA_OPAQUE },
+    {  37,   0, 255, SDL_ALPHA_OPAQUE },
+    {  43,   0, 255, SDL_ALPHA_OPAQUE },
+    {  49,   0, 255, SDL_ALPHA_OPAQUE },
+    {  55,   0, 255, SDL_ALPHA_OPAQUE },
+    {  61,   0, 255, SDL_ALPHA_OPAQUE },
+    {  67,   0, 255, SDL_ALPHA_OPAQUE },
+    {  73,   0, 255, SDL_ALPHA_OPAQUE },
+    {  79,   0, 255, SDL_ALPHA_OPAQUE },
+    {  85,   0, 255, SDL_ALPHA_OPAQUE },
+    {  91,   0, 255, SDL_ALPHA_OPAQUE },
+    {  97,   0, 255, SDL_ALPHA_OPAQUE },
+    { 103,   0, 255, SDL_ALPHA_OPAQUE },
+    { 109,   0, 255, SDL_ALPHA_OPAQUE },
+    { 115,   0, 255, SDL_ALPHA_OPAQUE },
+    { 121,   0, 255, SDL_ALPHA_OPAQUE },
+    { 127,   0, 255, SDL_ALPHA_OPAQUE },
+    { 133,   0, 255, SDL_ALPHA_OPAQUE },
+    { 139,   0, 255, SDL_ALPHA_OPAQUE },
+    { 145,   0, 255, SDL_ALPHA_OPAQUE },
+    { 151,   0, 255, SDL_ALPHA_OPAQUE },
+    { 157,   0, 255, SDL_ALPHA_OPAQUE },
+    { 163,   0, 255, SDL_ALPHA_OPAQUE },
+    { 169,   0, 255, SDL_ALPHA_OPAQUE },
+    { 175,   0, 255, SDL_ALPHA_OPAQUE },
+    { 181,   0, 255, SDL_ALPHA_OPAQUE },
+    { 187,   0, 255, SDL_ALPHA_OPAQUE },
+    { 193,   0, 255, SDL_ALPHA_OPAQUE },
+    { 199,   0, 255, SDL_ALPHA_OPAQUE },
+    { 205,   0, 255, SDL_ALPHA_OPAQUE },
+    { 211,   0, 255, SDL_ALPHA_OPAQUE },
+    { 217,   0, 255, SDL_ALPHA_OPAQUE },
+    { 223,   0, 255, SDL_ALPHA_OPAQUE },
+    { 229,   0, 255, SDL_ALPHA_OPAQUE },
+    { 235,   0, 255, SDL_ALPHA_OPAQUE },
+    { 241,   0, 255, SDL_ALPHA_OPAQUE },
+    { 247,   0, 255, SDL_ALPHA_OPAQUE },
+    { 253,   0, 255, SDL_ALPHA_OPAQUE },
+    { 255,   0, 251, SDL_ALPHA_OPAQUE },
+    { 255,   0, 245, SDL_ALPHA_OPAQUE },
+    { 255,   0, 239, SDL_ALPHA_OPAQUE },
+    { 255,   0, 233, SDL_ALPHA_OPAQUE },
+    { 255,   0, 227, SDL_ALPHA_OPAQUE },
+    { 255,   0, 221, SDL_ALPHA_OPAQUE },
+    { 255,   0, 215, SDL_ALPHA_OPAQUE },
+    { 255,   0, 209, SDL_ALPHA_OPAQUE },
+    { 255,   0, 203, SDL_ALPHA_OPAQUE },
+    { 255,   0, 197, SDL_ALPHA_OPAQUE },
+    { 255,   0, 191, SDL_ALPHA_OPAQUE },
+    { 255,   0, 185, SDL_ALPHA_OPAQUE },
+    { 255,   0, 179, SDL_ALPHA_OPAQUE },
+    { 255,   0, 173, SDL_ALPHA_OPAQUE },
+    { 255,   0, 167, SDL_ALPHA_OPAQUE },
+    { 255,   0, 161, SDL_ALPHA_OPAQUE },
+    { 255,   0, 155, SDL_ALPHA_OPAQUE },
+    { 255,   0, 149, SDL_ALPHA_OPAQUE },
+    { 255,   0, 143, SDL_ALPHA_OPAQUE },
+    { 255,   0, 137, SDL_ALPHA_OPAQUE },
+    { 255,   0, 131, SDL_ALPHA_OPAQUE },
+    { 255,   0, 125, SDL_ALPHA_OPAQUE },
+    { 255,   0, 119, SDL_ALPHA_OPAQUE },
+    { 255,   0, 113, SDL_ALPHA_OPAQUE },
+    { 255,   0, 107, SDL_ALPHA_OPAQUE },
+    { 255,   0, 101, SDL_ALPHA_OPAQUE },
+    { 255,   0,  95, SDL_ALPHA_OPAQUE },
+    { 255,   0,  89, SDL_ALPHA_OPAQUE },
+    { 255,   0,  83, SDL_ALPHA_OPAQUE },
+    { 255,   0,  77, SDL_ALPHA_OPAQUE },
+    { 255,   0,  71, SDL_ALPHA_OPAQUE },
+    { 255,   0,  65, SDL_ALPHA_OPAQUE },
+    { 255,   0,  59, SDL_ALPHA_OPAQUE },
+    { 255,   0,  53, SDL_ALPHA_OPAQUE },
+    { 255,   0,  47, SDL_ALPHA_OPAQUE },
+    { 255,   0,  41, SDL_ALPHA_OPAQUE },
+    { 255,   0,  35, SDL_ALPHA_OPAQUE },
+    { 255,   0,  29, SDL_ALPHA_OPAQUE },
+    { 255,   0,  23, SDL_ALPHA_OPAQUE },
+    { 255,   0,  17, SDL_ALPHA_OPAQUE },
+    { 255,   0,  11, SDL_ALPHA_OPAQUE },
+    { 255,   0,   5, SDL_ALPHA_OPAQUE }
+};
+
+static SDL_Renderer *renderer;
+static SDL_Palette *palette;
+static SDL_Texture *texture;
+static SDL_Texture *black_texture;
+static SDL_Texture *white_texture;
+static int palettePos = 0;
+static int paletteDir = -1;
+static bool done;
+
+static bool CreateTextures()
+{
+    Uint8 data[256];
+    int i;
+
+    palette = SDL_CreatePalette(256);
+    if (!palette) {
+        return false;
+    }
+
+    for (i = 0; i < SDL_arraysize(data); i++) {
+        data[i] = i;
+    }
+
+    texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_INDEX8, SDL_TEXTUREACCESS_STATIC, 256, 1);
+    if (!texture) {
+        return false;
+    }
+    SDL_UpdateTexture(texture, NULL, data, SDL_arraysize(data));
+    SDL_SetTexturePalette(texture, palette);
+
+    black_texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_INDEX8, SDL_TEXTUREACCESS_STATIC, 1, 1);
+    if (!black_texture) {
+        return false;
+    }
+    SDL_UpdateTexture(black_texture, NULL, data, SDL_arraysize(data));
+    SDL_SetTexturePalette(black_texture, palette);
+
+    white_texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_INDEX8, SDL_TEXTUREACCESS_STATIC, 1, 1);
+    if (!white_texture) {
+        return false;
+    }
+    SDL_UpdateTexture(white_texture, NULL, data, SDL_arraysize(data));
+    SDL_SetTexturePalette(white_texture, palette);
+
+    return true;
+}
+
+static void UpdatePalette(int pos)
+{
+    int paletteSize = (int)SDL_arraysize(Palette);
+
+    if (pos == 0) {
+        SDL_SetPaletteColors(palette, Palette, 0, paletteSize);
+    } else {
+        SDL_SetPaletteColors(palette, Palette + pos, 0, paletteSize - pos);
+        SDL_SetPaletteColors(palette, Palette, paletteSize - pos, pos);
+    }
+}
+
+static void loop(void)
+{
+    SDL_Event event;
+    SDL_FRect src = { 0.0f, 0.0f, 1.0f, 1.0f };
+    SDL_FRect dst1 = { 0.0f, 0.0f, 32.0f, 32.0f };
+    SDL_FRect dst2 = { WINDOW_WIDTH - 32.0f, 0.0f, 32.0f, 32.0f };
+    const SDL_Color black = { 0, 0, 0, SDL_ALPHA_OPAQUE };
+    const SDL_Color white = { 255, 255, 255, SDL_ALPHA_OPAQUE };
+
+    /* Check for events */
+    while (SDL_PollEvent(&event)) {
+        switch (event.type) {
+        case SDL_EVENT_KEY_UP:
+            switch (event.key.key) {
+            case SDLK_LEFT:
+                paletteDir = 1;
+                break;
+            case SDLK_RIGHT:
+                paletteDir = -1;
+                break;
+            case SDLK_ESCAPE:
+                done = true;
+                break;
+            default:
+                break;
+            }
+            break;
+        case SDL_EVENT_QUIT:
+            done = true;
+            break;
+        default:
+            break;
+        }
+    }
+
+    SDL_RenderClear(renderer);
+
+    /* Draw the rainbow texture */
+    UpdatePalette(palettePos);
+    palettePos += paletteDir;
+    if (palettePos < 0) {
+        palettePos = SDL_arraysize(Palette) - 1;
+    } else if (palettePos >= SDL_arraysize(Palette)) {
+        palettePos = 0;
+    }
+    SDL_RenderTexture(renderer, texture, NULL, NULL);
+
+    /* Draw one square with black, and one square with white
+     * This tests changing palette colors within a single frame
+     */
+    SDL_SetPaletteColors(palette, &black, 0, 1);
+    SDL_RenderTexture(renderer, black_texture, &src, &dst1);
+    SDL_SetPaletteColors(palette, &white, 0, 1);
+    SDL_RenderTexture(renderer, white_texture, &src, &dst2);
+
+    SDL_RenderPresent(renderer);
+    SDL_Delay(10);
+
+#ifdef SDL_PLATFORM_EMSCRIPTEN
+    if (done) {
+        emscripten_cancel_main_loop();
+    }
+#endif
+}
+
+int main(int argc, char *argv[])
+{
+    SDL_Window *window = NULL;
+    int return_code = -1;
+
+    if (argc > 1) {
+        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "USAGE: %s", argv[0]);
+        return_code = 1;
+        goto quit;
+    }
+
+    if (!SDL_CreateWindowAndRenderer("testpalette", WINDOW_WIDTH, WINDOW_HEIGHT, SDL_WINDOW_RESIZABLE, &window, &renderer)) {
+        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateWindowAndRenderer failed: %s", SDL_GetError());
+        return_code = 2;
+        goto quit;
+    }
+
+    if (!CreateTextures()) {
+        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't create textures: %s", SDL_GetError());
+        return_code = 3;
+        goto quit;
+    }
+
+    /* Main render loop */
+    done = false;
+
+#ifdef SDL_PLATFORM_EMSCRIPTEN
+    emscripten_set_main_loop(loop, 0, 1);
+#else
+    while (!done) {
+        loop();
+    }
+#endif
+    return_code = 0;
+quit:
+    SDL_DestroyPalette(palette);
+    SDL_DestroyRenderer(renderer);
+    SDL_DestroyWindow(window);
+    SDL_Quit();
+    return return_code;
+}

+ 2 - 0
test/testsymbols.c

@@ -1332,6 +1332,8 @@ const static struct {
     SDL_SYMBOL_ITEM(SDL_hid_get_properties),
     SDL_SYMBOL_ITEM(SDL_GetPixelFormatFromGPUTextureFormat),
     SDL_SYMBOL_ITEM(SDL_GetGPUTextureFormatFromPixelFormat),
+    SDL_SYMBOL_ITEM(SDL_SetTexturePalette),
+    SDL_SYMBOL_ITEM(SDL_GetTexturePalette),
     /* extra symbols go here (don't modify this line) */
     { NULL, NULL }
 };