SDL_ttf: Added support for custom font atlas and text drawing implementations

From 2e4f4ce1da66e0af80a93830b90c947ba0304167 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Sun, 29 Sep 2024 10:45:04 -0700
Subject: [PATCH] Added support for custom font atlas and text drawing
 implementations

Included are two implementations for SDL_Surface and SDL_Renderer
---
 CMakeLists.txt                    |   3 +
 VisualC/SDL_ttf.vcxproj           |  21 +-
 VisualC/SDL_ttf.vcxproj.filters   |   9 +
 examples/showfont.c               | 178 ++++++++--
 include/SDL3_ttf/SDL_textengine.h | 147 ++++++++
 include/SDL3_ttf/SDL_ttf.h        | 227 +++++++++++-
 src/SDL_hashtable.c               | 558 ++++++++++++++++++++++++++++++
 src/SDL_hashtable.h               |  64 ++++
 src/SDL_renderer_textengine.c     | 364 +++++++++++++++++++
 src/SDL_surface_textengine.c      | 369 ++++++++++++++++++++
 src/SDL_ttf.c                     | 410 +++++++++++++++++++++-
 src/SDL_ttf.sym                   |  11 +
 12 files changed, 2295 insertions(+), 66 deletions(-)
 create mode 100644 include/SDL3_ttf/SDL_textengine.h
 create mode 100644 src/SDL_hashtable.c
 create mode 100644 src/SDL_hashtable.h
 create mode 100644 src/SDL_renderer_textengine.c
 create mode 100644 src/SDL_surface_textengine.c

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 27e03f1e..db61f93d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -100,6 +100,9 @@ if (LIBC_IS_GLIBC AND CMAKE_SIZEOF_VOID_P EQUAL 4)
 endif()
 
 add_library(${sdl3_ttf_target_name}
+    src/SDL_hashtable.c
+    src/SDL_renderer_textengine.c
+    src/SDL_surface_textengine.c
     src/SDL_ttf.c
 )
 add_library(SDL3_ttf::${sdl3_ttf_target_name} ALIAS ${sdl3_ttf_target_name})
diff --git a/VisualC/SDL_ttf.vcxproj b/VisualC/SDL_ttf.vcxproj
index bca85e73..a5174617 100644
--- a/VisualC/SDL_ttf.vcxproj
+++ b/VisualC/SDL_ttf.vcxproj
@@ -371,24 +371,13 @@
     <ClCompile Include="..\external\harfbuzz\src\hb-uniscribe.cc" />
     <ClCompile Include="..\external\harfbuzz\src\hb-wasm-api.cc" />
     <ClCompile Include="..\external\harfbuzz\src\hb-wasm-shape.cc" />
-    <ClCompile Include="..\src\SDL_ttf.c">
-      <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
-      <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
-      <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
-      <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
-      <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
-      <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
-      <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|x64'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
-      <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Release|x64'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
-    </ClCompile>
+    <ClCompile Include="..\src\SDL_hashtable.c" />
+    <ClCompile Include="..\src\SDL_renderer_textengine.c" />
+    <ClCompile Include="..\src\SDL_surface_textengine.c" />
+    <ClCompile Include="..\src\SDL_ttf.c" />
   </ItemGroup>
   <ItemGroup>
-    <ResourceCompile Include="..\src\version.rc">
-      <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
-      <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
-      <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
-      <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Release|x64'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
-    </ResourceCompile>
+    <ResourceCompile Include="..\src\version.rc" />
   </ItemGroup>
   <ItemGroup>
     <ClInclude Include="..\include\SDL3_ttf\SDL_ttf.h" />
diff --git a/VisualC/SDL_ttf.vcxproj.filters b/VisualC/SDL_ttf.vcxproj.filters
index ea6ea142..ff341e02 100644
--- a/VisualC/SDL_ttf.vcxproj.filters
+++ b/VisualC/SDL_ttf.vcxproj.filters
@@ -21,6 +21,15 @@
     </Filter>
   </ItemGroup>
   <ItemGroup>
+    <ClCompile Include="..\src\SDL_hashtable.c">
+      <Filter>Sources</Filter>
+    </ClCompile>
+    <ClCompile Include="..\src\SDL_renderer_textengine.c">
+      <Filter>Sources</Filter>
+    </ClCompile>
+    <ClCompile Include="..\src\SDL_surface_textengine.c">
+      <Filter>Sources</Filter>
+    </ClCompile>
     <ClCompile Include="..\src\SDL_ttf.c">
       <Filter>Sources</Filter>
     </ClCompile>
diff --git a/examples/showfont.c b/examples/showfont.c
index ac9cc194..f468c7ae 100644
--- a/examples/showfont.c
+++ b/examples/showfont.c
@@ -35,7 +35,14 @@
 #define HEIGHT  480
 
 #define TTF_SHOWFONT_USAGE \
-"Usage: %s [-shaded] [-blended] [-wrapped] [-b] [-i] [-u] [-s] [-outline size] [-hintlight|-hintmono|-hintnone] [-nokerning] [-wrap] [-align left|center|right] [-fgcol r,g,b,a] [-bgcol r,g,b,a] <font>.ttf [ptsize] [text]\n"
+"Usage: %s [-textengine surface|renderer] [-shaded] [-blended] [-wrapped] [-b] [-i] [-u] [-s] [-outline size] [-hintlight|-hintmono|-hintnone] [-nokerning] [-wrap] [-align left|center|right] [-fgcol r,g,b,a] [-bgcol r,g,b,a] <font>.ttf [ptsize] [text]\n"
+
+typedef enum
+{
+    TextEngineNone,
+    TextEngineSurface,
+    TextEngineRenderer
+} TextEngine;
 
 typedef enum
 {
@@ -44,21 +51,58 @@ typedef enum
 } TextRenderMethod;
 
 typedef struct {
+    SDL_Window *window;
+    SDL_Surface *window_surface;
+    SDL_Renderer *renderer;
     SDL_Texture *caption;
     SDL_FRect captionRect;
     SDL_Texture *message;
     SDL_FRect messageRect;
+    TextEngine textEngine;
+    TTF_Text *text;
 } Scene;
 
-static void draw_scene(SDL_Renderer *renderer, Scene *scene)
+static void draw_scene(Scene *scene)
 {
+    SDL_Renderer *renderer = scene->renderer;
+
     /* Clear the background to background color */
     SDL_SetRenderDrawColor(renderer, 0xFF, 0xFF, 0xFF, 0xFF);
     SDL_RenderClear(renderer);
 
+    /* Flush the renderer so we can draw to the window surface in TextEngineSurface mode */
+    SDL_FlushRenderer(renderer);
+
+    if (scene->text) {
+        int i;
+        int w, h;
+
+        SDL_GetRenderOutputSize(renderer, &w, &h);
+
+        for (i = 0; i < 100; ++i) {
+            int x = SDL_rand(w) - scene->text->w / 2;
+            int y = SDL_rand(h) - scene->text->h / 2;
+
+            switch (scene->textEngine) {
+            case TextEngineSurface:
+                TTF_DrawSurfaceText(scene->text, x, y, scene->window_surface);
+                break;
+            case TextEngineRenderer:
+                TTF_DrawRendererText(scene->text, (float)x, (float)y);
+                break;
+            default:
+                break;
+            }
+        }
+    }
+
     SDL_RenderTexture(renderer, scene->caption, NULL, &scene->captionRect);
     SDL_RenderTexture(renderer, scene->message, NULL, &scene->messageRect);
     SDL_RenderPresent(renderer);
+
+    if (scene->window_surface) {
+        SDL_UpdateWindowSurface(scene->window);
+    }
 }
 
 static void cleanup(int exitcode)
@@ -71,9 +115,7 @@ static void cleanup(int exitcode)
 int main(int argc, char *argv[])
 {
     char *argv0 = argv[0];
-    SDL_Window *window;
-    SDL_Renderer *renderer;
-    TTF_Font *font;
+    TTF_Font *font = NULL;
     SDL_Surface *text = NULL;
     Scene scene;
     float ptsize;
@@ -83,6 +125,7 @@ int main(int argc, char *argv[])
     SDL_Color *forecol;
     SDL_Color *backcol;
     SDL_Event event;
+    TTF_TextEngine *engine = NULL;
     TextRenderMethod rendermethod;
     int renderstyle;
     int outline;
@@ -96,15 +139,28 @@ int main(int argc, char *argv[])
     /* Look for special execution mode */
     dump = 0;
     /* Look for special rendering types */
+    SDL_zero(scene);
     rendermethod = TextRenderShaded;
     renderstyle = TTF_STYLE_NORMAL;
     outline = 0;
     hinting = TTF_HINTING_NORMAL;
     kerning = 1;
+    wrap = 0;
     /* Default is black and white */
     forecol = &black;
     backcol = &white;
     for (i=1; argv[i] && argv[i][0] == '-'; ++i) {
+        if (SDL_strcmp(argv[i], "-textengine") == 0 && argv[i+1]) {
+            ++i;
+            if (SDL_strcmp(argv[i], "surface") == 0) {
+                scene.textEngine = TextEngineSurface;
+            }  else if (SDL_strcmp(argv[i], "renderer") == 0) {
+                scene.textEngine = TextEngineRenderer;
+            } else {
+                SDL_Log(TTF_SHOWFONT_USAGE, argv0);
+                return(1);
+            }
+        } else
         if (SDL_strcmp(argv[i], "-shaded") == 0) {
             rendermethod = TextRenderShaded;
         } else
@@ -123,7 +179,7 @@ int main(int argc, char *argv[])
         if (SDL_strcmp(argv[i], "-s") == 0) {
             renderstyle |= TTF_STYLE_STRIKETHROUGH;
         } else
-        if (SDL_strcmp(argv[i], "-outline") == 0) {
+        if (SDL_strcmp(argv[i], "-outline") == 0 && argv[i+1]) {
             if (SDL_sscanf(argv[++i], "%d", &outline) != 1) {
                 SDL_Log(TTF_SHOWFONT_USAGE, argv0);
                 return(1);
@@ -144,7 +200,7 @@ int main(int argc, char *argv[])
         if (SDL_strcmp(argv[i], "-wrap") == 0) {
             wrap = 1;
         } else
-        if (SDL_strcmp(argv[i], "-align") == 0) {
+        if (SDL_strcmp(argv[i], "-align") == 0 && argv[i+1]) {
             ++i;
             if (SDL_strcmp(argv[i], "left") == 0) {
                 align = TTF_HORIZONTAL_ALIGN_LEFT;
@@ -160,7 +216,7 @@ int main(int argc, char *argv[])
         if (SDL_strcmp(argv[i], "-dump") == 0) {
             dump = 1;
         } else
-        if (SDL_strcmp(argv[i], "-fgcol") == 0) {
+        if (SDL_strcmp(argv[i], "-fgcol") == 0 && argv[i+1]) {
             int r, g, b, a = SDL_ALPHA_OPAQUE;
             if (SDL_sscanf(argv[++i], "%d,%d,%d,%d", &r, &g, &b, &a) < 3) {
                 SDL_Log(TTF_SHOWFONT_USAGE, argv0);
@@ -171,7 +227,7 @@ int main(int argc, char *argv[])
             forecol->b = (Uint8)b;
             forecol->a = (Uint8)a;
         } else
-        if (SDL_strcmp(argv[i], "-bgcol") == 0) {
+        if (SDL_strcmp(argv[i], "-bgcol") == 0 && argv[i+1]) {
             int r, g, b, a = SDL_ALPHA_OPAQUE;
             if (SDL_sscanf(argv[++i], "%d,%d,%d,%d", &r, &g, &b, &a) < 3) {
                 SDL_Log(TTF_SHOWFONT_USAGE, argv0);
@@ -242,8 +298,28 @@ int main(int argc, char *argv[])
     }
 
     /* Create a window */
-    if (!SDL_CreateWindowAndRenderer("showfont demo", WIDTH, HEIGHT, 0, &window, &renderer)) {
-        SDL_Log("SDL_CreateWindowAndRenderer() failed: %s\n", SDL_GetError());
+    scene.window = SDL_CreateWindow("showfont demo", WIDTH, HEIGHT, 0);
+    if (!scene.window) {
+        SDL_Log("SDL_CreateWindow() failed: %s\n", SDL_GetError());
+        cleanup(2);
+    }
+    if (scene.textEngine == TextEngineSurface) {
+        scene.window_surface = SDL_GetWindowSurface(scene.window);
+        if (!scene.window_surface) {
+            SDL_Log("SDL_CreateWindowSurface() failed: %s\n", SDL_GetError());
+            cleanup(2);
+        }
+        SDL_SetWindowSurfaceVSync(scene.window, 1);
+
+        scene.renderer = SDL_CreateSoftwareRenderer(scene.window_surface);
+    } else {
+        scene.renderer = SDL_CreateRenderer(scene.window, NULL);
+        if (scene.renderer) {
+            SDL_SetRenderVSync(scene.renderer, 1);
+        }
+    }
+    if (!scene.renderer) {
+        SDL_Log("SDL_CreateRenderer() failed: %s\n", SDL_GetError());
         cleanup(2);
     }
 
@@ -262,7 +338,7 @@ int main(int argc, char *argv[])
         scene.captionRect.y = 4.0f;
         scene.captionRect.w = (float)text->w;
         scene.captionRect.h = (float)text->h;
-        scene.caption = SDL_CreateTextureFromSurface(renderer, text);
+        scene.caption = SDL_CreateTextureFromSurface(scene.renderer, text);
         SDL_DestroySurface(text);
     }
 
@@ -297,39 +373,75 @@ int main(int argc, char *argv[])
     scene.messageRect.y = (float)((HEIGHT - text->h)/2);
     scene.messageRect.w = (float)text->w;
     scene.messageRect.h = (float)text->h;
-    scene.message = SDL_CreateTextureFromSurface(renderer, text);
+    scene.message = SDL_CreateTextureFromSurface(scene.renderer, text);
     SDL_Log("Font is generally %d big, and string is %d big\n",
                         TTF_GetFontHeight(font), text->h);
 
-    draw_scene(renderer, &scene);
+    switch (scene.textEngine) {
+    case TextEngineSurface:
+        engine = TTF_CreateSurfaceTextEngine();
+        if (!engine) {
+            SDL_Log("Couldn't create surface text engine: %s\n", SDL_GetError());
+        }
+        break;
+    case TextEngineRenderer:
+        engine = TTF_CreateRendererTextEngine(scene.renderer);
+        if (!engine) {
+            SDL_Log("Couldn't create renderer text engine: %s\n", SDL_GetError());
+        }
+        break;
+    default:
+        break;
+    }
+    if (engine) {
+        if (wrap) {
+            scene.text = TTF_CreateText_Wrapped(engine, font, message, 0, 0);
+        } else {
+            scene.text = TTF_CreateText(engine, font, message, 0);
+        }
+        if (scene.text) {
+            scene.text->color.r = forecol->r / 255.0f;
+            scene.text->color.g = forecol->g / 255.0f;
+            scene.text->color.b = forecol->b / 255.0f;
+            scene.text->color.a = forecol->a / 255.0f;
+        }
+    }
 
     /* Wait for a keystroke, and blit text on mouse press */
     done = 0;
     while (!done) {
-        if (!SDL_WaitEvent(&event)) {
-            SDL_Log("SDL_PullEvent() error: %s\n", SDL_GetError());
-            done = 1;
-            continue;
-        }
-        switch (event.type) {
-            case SDL_EVENT_MOUSE_BUTTON_DOWN:
-                scene.messageRect.x = (float)(event.button.x - text->w/2);
-                scene.messageRect.y = (float)(event.button.y - text->h/2);
-                scene.messageRect.w = (float)text->w;
-                scene.messageRect.h = (float)text->h;
-                draw_scene(renderer, &scene);
-                break;
+        while (SDL_PollEvent(&event)) {
+            switch (event.type) {
+                case SDL_EVENT_MOUSE_BUTTON_DOWN:
+                    scene.messageRect.x = (float)(event.button.x - text->w/2);
+                    scene.messageRect.y = (float)(event.button.y - text->h/2);
+                    scene.messageRect.w = (float)text->w;
+                    scene.messageRect.h = (float)text->h;
+                    break;
 
-            case SDL_EVENT_KEY_DOWN:
-            case SDL_EVENT_QUIT:
-                done = 1;
-                break;
-            default:
-                break;
+                case SDL_EVENT_KEY_DOWN:
+                case SDL_EVENT_QUIT:
+                    done = 1;
+                    break;
+                default:
+                    break;
+            }
         }
+        draw_scene(&scene);
     }
     SDL_DestroySurface(text);
     TTF_CloseFont(font);
+    TTF_DestroyText(scene.text);
+    switch (scene.textEngine) {
+    case TextEngineSurface:
+        TTF_DestroySurfaceTextEngine(engine);
+        break;
+    case TextEngineRenderer:
+        TTF_DestroyRendererTextEngine(engine);
+        break;
+    default:
+        break;
+    }
     SDL_DestroyTexture(scene.caption);
     SDL_DestroyTexture(scene.message);
     cleanup(0);
diff --git a/include/SDL3_ttf/SDL_textengine.h b/include/SDL3_ttf/SDL_textengine.h
new file mode 100644
index 00000000..b3e32ba4
--- /dev/null
+++ b/include/SDL3_ttf/SDL_textengine.h
@@ -0,0 +1,147 @@
+/*
+  SDL_ttf:  A companion library to SDL for working with TrueType (tm) fonts
+  Copyright (C) 2001-2024 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.
+*/
+
+
+/**
+ *  \file SDL_textengine.h
+ *
+ * Definitions for implementations of the TTF_TextEngine interface.
+ */
+#ifndef SDL_TTF_TEXTENGINE_H_
+#define SDL_TTF_TEXTENGINE_H_
+
+#include <SDL3/SDL.h>
+#include <SDL3/SDL_begin_code.h>
+
+/* Set up for C function definitions, even when using C++ */
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* Text created with the text engine */
+typedef struct TTF_Text TTF_Text;
+
+/**
+ * A font atlas draw command.
+ *
+ * \since This enum is available since SDL_ttf 3.0.0.
+ */
+typedef enum TTF_DrawCommand
+{
+    TTF_DRAW_COMMAND_NOOP,
+    TTF_DRAW_COMMAND_FILL,
+    TTF_DRAW_COMMAND_COPY
+} TTF_DrawCommand;
+
+/**
+ * A filled rectangle draw operation.
+ *
+ * \since This struct is available since SDL_ttf 3.0.0.
+ *
+ * \sa TTF_DrawOperation
+ */
+typedef struct TTF_FillOperation
+{
+    TTF_DrawCommand cmd;    /**< TTF_DRAW_COMMAND_FILL */
+    SDL_Rect rect;          /**< The rectangle to fill, in pixels. The x coordinate is relative to the left side of the text area, going right, and the y coordinate is relative to the top side of the text area, going down. */
+} TTF_FillOperation;
+
+/**
+ * A texture copy draw operation.
+ *
+ * \since This struct is available since SDL_ttf 3.0.0.
+ *
+ * \sa TTF_DrawOperation
+ */
+typedef struct TTF_CopyOperation
+{
+    TTF_DrawCommand cmd;    /**< TTF_DRAW_COMMAND_COPY */
+    Uint32 glyph_index;     /**< The glyph index of the glyph to be drawn, can be passed to TTF_GetGlyphForIndex() */
+    SDL_Rect src;           /**< The area within the glyph to be drawn */
+    SDL_Rect dst;           /**< The drawing coordinates of the glyph, in pixels. The x coordinate is relative to the left side of the text area, going right, and the y coordinate is relative to the top side of the text area, going down. */
+    void *reserved;
+} TTF_CopyOperation;
+
+/**
+ * A text engine draw operation.
+ *
+ * \since This struct is available since SDL_ttf 3.0.0.
+ */
+typedef union TTF_DrawOperation
+{
+    TTF_DrawCommand cmd;
+    TTF_FillOperation fill;
+    TTF_CopyOperation copy;
+} TTF_DrawOperation;
+
+/**
+ * A text engine interface.
+ *
+ * This structure should be initialized using SDL_INIT_INTERFACE()
+ *
+ * \since This struct is available since SDL_ttf 3.0.0.
+ *
+ * \sa SDL_INIT_INTERFACE
+ */
+typedef struct TTF_TextEngine
+{
+    Uint32 version;     /**< The version of this interface */
+
+    void *userdata;     /**< User data pointer passed to callbacks */
+
+    /* Create a text representation from draw instructions.
+     *
+     * All fields of `text` except `internal` will already be filled out.
+     *
+     * \param userdata the userdata pointer in this interface.
+     * \param font the font being used.
+     * \param font_generation the unique ID of the font generation being used. This changes whenever the font changes size or style and needs new glyphs, and is unique across all fonts.
+     * \param ops the text drawing operations
+     * \param num_ops the number of text drawing operations
+     */
+    bool (SDLCALL *CreateText)(void *userdata, TTF_Font *font, Uint32 font_generation, TTF_Text *text, TTF_DrawOperation *ops, int num_ops);
+
+    /**
+     * Destroy a text representation.
+     */
+    void (SDLCALL *DestroyText)(void *userdata, TTF_Text *text);
+
+} TTF_TextEngine;
+
+/* Check the size of TTF_TextEngine
+ *
+ * If this assert fails, either the compiler is padding to an unexpected size,
+ * or the interface has been updated and this should be updated to match and
+ * the code using this interface should be updated to handle the old version.
+ */
+SDL_COMPILE_TIME_ASSERT(TTF_TextEngine_SIZE,
+    (sizeof(void *) == 4 && sizeof(TTF_TextEngine) == 16) ||
+    (sizeof(void *) == 8 && sizeof(TTF_TextEngine) == 32));
+
+
+/* Ends C function definitions when using C++ */
+#ifdef __cplusplus
+}
+#endif
+#include <SDL3/SDL_close_code.h>
+
+#endif /* SDL_TTF_TEXTENGINE_H_ */
+
diff --git a/include/SDL3_ttf/SDL_ttf.h b/include/SDL3_ttf/SDL_ttf.h
index 14d9fef1..25982f55 100644
--- a/include/SDL3_ttf/SDL_ttf.h
+++ b/include/SDL3_ttf/SDL_ttf.h
@@ -788,6 +788,38 @@ extern SDL_DECLSPEC bool TTF_SetFontLanguage(TTF_Font *font, const char *languag
  */
 extern SDL_DECLSPEC bool SDLCALL TTF_FontHasGlyph(TTF_Font *font, Uint32 ch);
 
+/**
+ * Get the pixel image for a UNICODE codepoint.
+ *
+ * \param font the font to query.
+ * \param ch the codepoint to check.
+ * \returns an SDL_Surface containing the glyph, or NULL on failure; call SDL_GetError() for more
+ *          information.
+ *
+ * \threadsafety This function should be called on the thread that created the
+ *               font.
+ *
+ * \since This function is available since SDL_ttf 3.0.0.
+ */
+extern SDL_DECLSPEC SDL_Surface * SDLCALL TTF_GetGlyphImage(TTF_Font *font, Uint32 ch);
+
+/**
+ * Get the pixel image for a character index.
+ *
+ * This is useful for text engine implementations, which can call this with the `glyph_index` in a TTF_CopyOperation
+ *
+ * \param font the font to query.
+ * \param glyph_index the index of the glyph to return.
+ * \returns an SDL_Surface containing the glyph, or NULL on failure; call SDL_GetError() for more
+ *          information.
+ *
+ * \threadsafety This function should be called on the thread that created the
+ *               font.
+ *
+ * \since This function is available since SDL_ttf 3.0.0.
+ */
+extern SDL_DECLSPEC SDL_Surface * SDLCALL TTF_GetGlyphImageForIndex(TTF_Font *font, Uint32 glyph_index);
+
 /**
  * Query the metrics (dimensions) of a font's glyph for a UNICODE codepoint.
  *
@@ -1190,6 +1222,200 @@ extern SDL_DECLSPEC SDL_Surface * SDLCALL TTF_RenderText_LCD_Wrapped(TTF_Font *f
 extern SDL_DECLSPEC SDL_Surface * SDLCALL TTF_RenderGlyph_LCD(TTF_Font *font, Uint32 ch, SDL_Color fg, SDL_Color bg);
 
 
+/* A text engine used to create text objects
+ *
+ * This is a public interface that can be used by applications and libraries. See <SDL3_ttf/SDL_textengine.h> for details.
+ */
+typedef struct TTF_TextEngine TTF_TextEngine;
+
+/**
+ * Text created with TTF_CreateText()
+ *
+ * \since This struct is available since SDL_ttf 3.0.0.
+ *
+ * \sa TTF_CreateText
+ * \sa TTF_CreateText_Wrapped
+ * \sa TTF_DestroyText
+ */
+typedef struct TTF_Text
+{
+    char *label;            /**< A label that you can allocate with SDL_strdup() for debugging purposes, and will be automatically freed in TTF_DestroyText(). */
+    int w;                  /**< The width of this text, in pixels, read-only. */
+    int h;                  /**< The height of this text, in pixels, read-only. */
+    SDL_FColor color;       /**< The color of the text, read-write. You can change this anytime. */
+    TTF_TextEngine *engine; /**< The engine used to create this text, read-only. */
+    void *internal;         /**< The internal representation of this text, read-only */
+} TTF_Text;
+
+/**
+ * Create a text engine for drawing text on SDL surfaces.
+ *
+ * \returns a TTF_TextEngine object or NULL on failure; call SDL_GetError() for more information.
+ *
+ * \threadsafety It is safe to call this function from any thread.
+ *
+ * \since This function is available since SDL_ttf 3.0.0.
+ *
+ * \sa TTF_DestroySurfaceTextEngine
+ * \sa TTF_DrawSurfaceText
+ */
+extern SDL_DECLSPEC TTF_TextEngine * SDLCALL TTF_CreateSurfaceTextEngine(void);
+
+/**
+ * Draw text to an SDL surface.
+ *
+ * `text` must have been created using a TTF_TextEngine from TTF_CreateSurfaceTextEngine().
+ *
+ * \param text the text to draw.
+ * \param x the x coordinate in pixels, positive from the left edge towards the right.
+ * \param y the y coordinate in pixels, positive from the top edge towards the bottom.
+ * \param surface the surface to draw on.
+ * \returns true on success or false on failure; call SDL_GetError() for more
+ *          information.
+ *
+ * \threadsafety This function should be called on the thread that created the text.
+ *
+ * \since This function is available since SDL_ttf 3.0.0.
+ *
+ * \sa TTF_CreateSurfaceTextEngine
+ * \sa TTF_CreateText
+ * \sa TTF_CreateText_Wrapped
+ */
+extern SDL_DECLSPEC bool SDLCALL TTF_DrawSurfaceText(TTF_Text *text, int x, int y, SDL_Surface *surface);
+
+/**
+ * Destroy a text engine created for drawing text on SDL surfaces.
+ *
+ * All text created by this engine should be destroyed before calling this function.
+ *
+ * \param engine a TTF_TextEngine object created with TTF_CreateSurfaceTextEngine().
+ *
+ * \threadsafety This function should be called on the thread that created the engine.
+ *
+ * \since This function is available since SDL_ttf 3.0.0.
+ *
+ * \sa TTF_CreateSurfaceTextEngine
+ */
+extern SDL_DECLSPEC void SDLCALL TTF_DestroySurfaceTextEngine(TTF_TextEngine *engine);
+
+/**
+ * Create a text engine for drawing text on an SDL renderer.
+ *
+ * \param renderer the renderer to use for creating textures and drawing text.
+ * \returns a TTF_TextEngine object or NULL on failure; call SDL_GetError() for more information.
+ *
+ * \threadsafety This function should be called on the thread that created the renderer.
+ *
+ * \since This function is available since SDL_ttf 3.0.0.
+ *
+ * \sa TTF_DestroyRendererTextEngine
+ */
+extern SDL_DECLSPEC TTF_TextEngine * SDLCALL TTF_CreateRendererTextEngine(SDL_Renderer *renderer);
+
+/**
+ * Draw text to an SDL renderer.
+ *
+ * `text` must have been created using a TTF_TextEngine from TTF_CreateRendererTextEngine(), and will draw using the renderer passed to that function.
+ *
+ * \param text the text to draw.
+ * \param x the x coordinate in pixels, positive from the left edge towards the right.
+ * \param y the y coordinate in pixels, positive from the top edge towards the bottom.
+ * \returns true on success or false on failure; call SDL_GetError() for more
+ *          information.
+ *
+ * \threadsafety This function should be called on the thread that created the text.
+ *
+ * \since This function is available since SDL_ttf 3.0.0.
+ *
+ * \sa TTF_CreateRendererTextEngine
+ * \sa TTF_CreateText
+ * \sa TTF_CreateText_Wrapped
+ */
+extern SDL_DECLSPEC bool SDLCALL TTF_DrawRendererText(TTF_Text *text, float x, float y);
+
+/**
+ * Destroy a text engine created for drawing text on an SDL renderer.
+ *
+ * All text created by this engine should be destroyed before calling this function.
+ *
+ * \param engine a TTF_TextEngine object created with TTF_CreateRendererTextEngine().
+ *
+ * \threadsafety This function should be called on the thread that created the engine.
+ *
+ * \since This function is available since SDL_ttf 3.0.0.
+ *
+ * \sa TTF_CreateRendererTextEngine
+ */
+extern SDL_DECLSPEC void SDLCALL TTF_DestroyRendererTextEngine(TTF_TextEngine *engine);
+
+/**
+ * Create a text object from UTF-8 text and a text engine.
+ *
+ * This will not word-wrap the string; you'll get a surface with a single line
+ * of text, as long as the string requires. You can use
+ * TTF_CreateText_Wrapped() instead if you need to wrap the output to
+ * multiple lines.
+ *
+ * This will not wrap on newline characters.
+ *
+ * \param engine the text engine to use when creating the text object.
+ * \param font the font to render with.
+ * \param text the text to use, in UTF-8 encoding.
+ * \param length the length of the text, in bytes, or 0 for null terminated
+ *               text.
+ * \returns a TTF_Text object or NULL on failure; call SDL_GetError() for more information.
+ *
+ * \threadsafety This function should be called on the thread that created the
+ *               font and text engine.
+ *
+ * \since This function is available since SDL_ttf 3.0.0.
+ *
+ * \sa TTF_CreateText_Wrapped
+ * \sa TTF_DestroyText
+ */
+extern SDL_DECLSPEC TTF_Text * SDLCALL TTF_CreateText(TTF_TextEngine *engine, TTF_Font *font, const char *text, size_t length);
+
+/**
+ * Create a text object from word-wrapped UTF-8 text and a text engine.
+ *
+ * Text is wrapped to multiple lines on line endings and on word boundaries if
+ * it extends beyond `wrapLength` in pixels.
+ *
+ * If wrapLength is 0, this function will only wrap on newline characters.
+ *
+ * \param engine the text engine to use when creating the text object.
+ * \param font the font to render with.
+ * \param text the text to use, in UTF-8 encoding.
+ * \param length the length of the text, in bytes, or 0 for null terminated
+ *               text.
+ * \param wrapLength the maximum width of the text surface or 0 to wrap on
+ *                   newline characters.
+ * \returns a TTF_Text object or NULL on failure; call SDL_GetError() for more information.
+ *
+ * \threadsafety This function should be called on the thread that created the
+ *               font.
+ *
+ * \since This function is available since SDL_ttf 3.0.0.
+ *
+ * \sa TTF_CreateText
+ * \sa TTF_DestroyText
+ */
+extern SDL_DECLSPEC TTF_Text * SDLCALL TTF_CreateText_Wrapped(TTF_TextEngine *engine, TTF_Font *font, const char *text, size_t length, int wrapLength);
+
+/**
+ * Destroy a text object created by a text engine.
+ *
+ * \param text the text to destroy.
+ *
+ * \threadsafety This function should be called on the thread that created the text.
+ *
+ * \since This function is available since SDL_ttf 3.0.0.
+ *
+ * \sa TTF_CreateText
+ * \sa TTF_CreateText_Wrapped
+ */
+extern SDL_DECLSPEC void SDLCALL TTF_DestroyText(TTF_Text *text);
+
 /**
  * Dispose of a previously-created font.
  *
@@ -1275,4 +1501,3 @@ extern SDL_DECLSPEC int SDLCALL TTF_WasInit(void);
 
 #endif /* SDL_TTF_H_ */
 
-/* vi: set ts=4 sw=4 expandtab: */
diff --git a/src/SDL_hashtable.c b/src/SDL_hashtable.c
new file mode 100644
index 00000000..797264a2
--- /dev/null
+++ b/src/SDL_hashtable.c
@@ -0,0 +1,558 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2024 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 distributi

(Patch may be truncated, please check the link at the top of this post.)