SDL_ttf: Added TTF_CreateRendererTextEngineWithProperties and TTF_CreateGPUTextEngineWithProperties (#485)

From 48a7fa12d0aec7993cce65f51038fe48178e6f87 Mon Sep 17 00:00:00 2001
From: Captain <[EMAIL REDACTED]>
Date: Wed, 29 Jan 2025 21:00:59 +0530
Subject: [PATCH] Added TTF_CreateRendererTextEngineWithProperties and
 TTF_CreateGPUTextEngineWithProperties (#485)

---
 include/SDL3_ttf/SDL_ttf.h    | 50 +++++++++++++++++++++++++++++
 src/SDL_gpu_textengine.c      | 60 +++++++++++++++++++++++------------
 src/SDL_renderer_textengine.c | 58 ++++++++++++++++++++++-----------
 src/SDL_ttf.sym               |  2 ++
 4 files changed, 131 insertions(+), 39 deletions(-)

diff --git a/include/SDL3_ttf/SDL_ttf.h b/include/SDL3_ttf/SDL_ttf.h
index 30989f5f..f6e7acca 100644
--- a/include/SDL3_ttf/SDL_ttf.h
+++ b/include/SDL3_ttf/SDL_ttf.h
@@ -1675,6 +1675,31 @@ extern SDL_DECLSPEC void SDLCALL TTF_DestroySurfaceTextEngine(TTF_TextEngine *en
  */
 extern SDL_DECLSPEC TTF_TextEngine * SDLCALL TTF_CreateRendererTextEngine(SDL_Renderer *renderer);
 
+/**
+ * Create a text engine for drawing text on an SDL renderer, with the specified properties.
+ * 
+ * These are the supported properties:
+ *
+ * - `TTF_PROP_RENDERER_TEXT_ENGINE_RENDERER`: the renderer to use for creating textures
+ *   and drawing text
+ * - `TTF_PROP_RENDERER_TEXT_ENGINE_ATLAS_TEXTURE_SIZE`: the size of the texture atlas
+ *
+ * \param props the properties to use
+ * \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_CreateRendererTextEngineWithProperties(SDL_PropertiesID props);
+
+#define TTF_PROP_RENDERER_TEXT_ENGINE_RENDERER                 "SDL_ttf.renderer_text_engine.create.renderer"
+#define TTF_PROP_RENDERER_TEXT_ENGINE_ATLAS_TEXTURE_SIZE       "SDL_ttf.renderer_text_engine.create.atlas_texture_size"
+
 /**
  * Draw text to an SDL renderer.
  *
@@ -1735,6 +1760,31 @@ extern SDL_DECLSPEC void SDLCALL TTF_DestroyRendererTextEngine(TTF_TextEngine *e
  */
 extern SDL_DECLSPEC TTF_TextEngine * SDLCALL TTF_CreateGPUTextEngine(SDL_GPUDevice *device);
 
+/**
+ * Create a text engine for drawing text with the SDL GPU API, with the specified properties.
+ * 
+ * These are the supported properties:
+ *
+ * - `TTF_PROP_GPU_TEXT_ENGINE_DEVICE`: the SDL_GPUDevice to use for creating
+ *   textures and drawing text.
+ * - `TTF_PROP_GPU_TEXT_ENGINE_ATLAS_TEXTURE_SIZE`: the size of the texture atlas
+ *
+ * \param props the properties to use.
+ * \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
+ *               device.
+ *
+ * \since This function is available since SDL_ttf 3.0.0.
+ *
+ * \sa TTF_DestroyGPUTextEngine
+ */
+extern SDL_DECLSPEC TTF_TextEngine * SDLCALL TTF_CreateGPUTextEngineWithProperties(SDL_PropertiesID props);
+
+#define TTF_PROP_GPU_TEXT_ENGINE_DEVICE                   "SDL_ttf.gpu_text_engine.create.device"
+#define TTF_PROP_GPU_TEXT_ENGINE_ATLAS_TEXTURE_SIZE       "SDL_ttf.gpu_text_engine.create.atlas_texture_size"
+
 /**
  * Draw sequence returned by TTF_GetGPUTextDrawData
  *
diff --git a/src/SDL_gpu_textengine.c b/src/SDL_gpu_textengine.c
index c3486fcd..29e6f145 100644
--- a/src/SDL_gpu_textengine.c
+++ b/src/SDL_gpu_textengine.c
@@ -25,8 +25,6 @@
 #include "SDL_hashtable.h"
 #include "SDL_hashtable_ttf.h"
 
-#define ATLAS_TEXTURE_SIZE 1024
-
 #define STB_RECT_PACK_IMPLEMENTATION
 #define STBRP_STATIC
 #define STBRP_SORT   SDL_qsort
@@ -75,6 +73,7 @@ typedef struct TTF_GPUTextEngineData
     SDL_GPUDevice *device;
     SDL_HashTable *fonts;
     AtlasTexture *atlas;
+    int atlas_texture_size;
     TTF_GPUTextEngineWinding winding;
 } TTF_GPUTextEngineData;
 
@@ -173,7 +172,7 @@ static void DestroyAtlas(SDL_GPUDevice *device, AtlasTexture *atlas)
     SDL_free(atlas);
 }
 
-static AtlasTexture *CreateAtlas(SDL_GPUDevice *device)
+static AtlasTexture *CreateAtlas(SDL_GPUDevice *device, int atlas_texture_size)
 {
     AtlasTexture *atlas = (AtlasTexture *)SDL_calloc(1, sizeof(*atlas));
     if (!atlas) {
@@ -184,8 +183,8 @@ static AtlasTexture *CreateAtlas(SDL_GPUDevice *device)
     info.type = SDL_GPU_TEXTURETYPE_2D;
     info.format = SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM;
     info.usage = SDL_GPU_TEXTUREUSAGE_SAMPLER | SDL_GPU_TEXTUREUSAGE_COLOR_TARGET;
-    info.width = ATLAS_TEXTURE_SIZE;
-    info.height = ATLAS_TEXTURE_SIZE;
+    info.width = atlas_texture_size;
+    info.height = atlas_texture_size;
     info.layer_count_or_depth = 1;
     info.num_levels = 1;
 
@@ -207,13 +206,13 @@ static AtlasTexture *CreateAtlas(SDL_GPUDevice *device)
     SDL_EndGPURenderPass(rpass);
     SDL_SubmitGPUCommandBuffer(cbuf);
 
-    int num_nodes = ATLAS_TEXTURE_SIZE / 4;
+    int num_nodes = atlas_texture_size / 4;
     atlas->packing_nodes = (stbrp_node *)SDL_calloc(num_nodes, sizeof(*atlas->packing_nodes));
     if (!atlas->packing_nodes) {
         DestroyAtlas(device, atlas);
         return NULL;
     }
-    stbrp_init_target(&atlas->packer, ATLAS_TEXTURE_SIZE, ATLAS_TEXTURE_SIZE, atlas->packing_nodes, num_nodes);
+    stbrp_init_target(&atlas->packer, atlas_texture_size, atlas_texture_size, atlas->packing_nodes, num_nodes);
     stbrp_setup_heuristic(&atlas->packer, STBRP_HEURISTIC_Skyline_default);
 
     return atlas;
@@ -251,7 +250,7 @@ static void ReleaseGlyph(AtlasGlyph *glyph)
     }
 }
 
-static AtlasGlyph *CreateGlyph(AtlasTexture *atlas, const stbrp_rect *area)
+static AtlasGlyph *CreateGlyph(AtlasTexture *atlas, int atlas_texture_size, const stbrp_rect *area)
 {
     AtlasGlyph *glyph = (AtlasGlyph *)SDL_calloc(1, sizeof(*glyph));
     if (!glyph) {
@@ -266,10 +265,10 @@ static AtlasGlyph *CreateGlyph(AtlasTexture *atlas, const stbrp_rect *area)
     glyph->rect.w = area->w - 1;
     glyph->rect.h = area->h - 1;
 
-    const float minu = (float)glyph->rect.x / ATLAS_TEXTURE_SIZE;
-    const float minv = (float)glyph->rect.y / ATLAS_TEXTURE_SIZE;
-    const float maxu = (float)(glyph->rect.x + glyph->rect.w) / ATLAS_TEXTURE_SIZE;
-    const float maxv = (float)(glyph->rect.y + glyph->rect.h) / ATLAS_TEXTURE_SIZE;
+    const float minu = (float)glyph->rect.x / atlas_texture_size;
+    const float minv = (float)glyph->rect.y / atlas_texture_size;
+    const float maxu = (float)(glyph->rect.x + glyph->rect.w) / atlas_texture_size;
+    const float maxv = (float)(glyph->rect.y + glyph->rect.h) / atlas_texture_size;
     glyph->texcoords[0] = minu;
     glyph->texcoords[1] = minv;
     glyph->texcoords[2] = maxu;
@@ -439,7 +438,7 @@ static bool ResolveMissingGlyphs(TTF_GPUTextEngineData *enginedata, AtlasTexture
             continue;
         }
 
-        AtlasGlyph *glyph = CreateGlyph(atlas, &missing[i]);
+        AtlasGlyph *glyph = CreateGlyph(atlas, enginedata->atlas_texture_size, &missing[i]);
         if (!glyph) {
             return false;
         }
@@ -473,7 +472,7 @@ static bool ResolveMissingGlyphs(TTF_GPUTextEngineData *enginedata, AtlasTexture
     }
 
     if (!atlas->next) {
-        atlas->next = CreateAtlas(enginedata->device);
+        atlas->next = CreateAtlas(enginedata->device, enginedata->atlas_texture_size);
         if (!atlas->next) {
             return false;
         }
@@ -487,6 +486,7 @@ static bool CreateMissingGlyphs(TTF_GPUTextEngineData *enginedata, TTF_GPUTextEn
     SDL_Surface **surfaces = NULL;
     SDL_HashTable *checked = NULL;
     bool result = false;
+    int atlas_texture_size = enginedata->atlas_texture_size;
 
     // Build a list of missing glyphs
     missing = (stbrp_rect *)SDL_calloc(num_missing, sizeof(*missing));
@@ -521,10 +521,10 @@ static bool CreateMissingGlyphs(TTF_GPUTextEngineData *enginedata, TTF_GPUTextEn
             if (!surfaces[i]) {
                 goto done;
             }
-            if (surfaces[i]->w > ATLAS_TEXTURE_SIZE || surfaces[i]->h > ATLAS_TEXTURE_SIZE) {
+            if (surfaces[i]->w > atlas_texture_size || surfaces[i]->h > atlas_texture_size) {
                 SDL_SetError("Glyph surface %dx%d larger than atlas texture %dx%d",
                     surfaces[i]->w, surfaces[i]->h,
-                    ATLAS_TEXTURE_SIZE, ATLAS_TEXTURE_SIZE);
+                    atlas_texture_size, atlas_texture_size);
                 goto done;
             }
 
@@ -542,7 +542,7 @@ static bool CreateMissingGlyphs(TTF_GPUTextEngineData *enginedata, TTF_GPUTextEn
 
     // Create the texture atlas if necessary
     if (!enginedata->atlas) {
-        enginedata->atlas = CreateAtlas(enginedata->device);
+        enginedata->atlas = CreateAtlas(enginedata->device, atlas_texture_size);
         if (!enginedata->atlas) {
             goto done;
         }
@@ -865,13 +865,14 @@ static void NukeFontData(const void *key, const void *value, void *unused)
     DestroyFontData(data);
 }
 
-static TTF_GPUTextEngineData *CreateEngineData(SDL_GPUDevice *device)
+static TTF_GPUTextEngineData *CreateEngineData(SDL_GPUDevice *device, int atlas_texture_size)
 {
     TTF_GPUTextEngineData *data = (TTF_GPUTextEngineData *)SDL_calloc(1, sizeof(*data));
     if (!data) {
         return NULL;
     }
     data->device = device;
+    data->atlas_texture_size = atlas_texture_size;
     data->winding = TTF_GPU_TEXTENGINE_WINDING_CLOCKWISE;
 
     data->fonts = SDL_CreateHashTable(NULL, 4, SDL_HashPointer, SDL_KeyMatchPointer, NukeFontData, false, false);
@@ -939,8 +940,21 @@ void TTF_DestroyGPUTextEngine(TTF_TextEngine *engine)
 
 TTF_TextEngine *TTF_CreateGPUTextEngine(SDL_GPUDevice *device)
 {
+    SDL_PropertiesID props = SDL_CreateProperties();
+    if (props == 0) {
+        SDL_SetError("Failed to create GPU text engine.");
+        return NULL;
+    }
+    SDL_SetPointerProperty(props, TTF_PROP_GPU_TEXT_ENGINE_DEVICE, device);
+
+    return TTF_CreateGPUTextEngineWithProperties(props);
+}
+
+TTF_TextEngine *TTF_CreateGPUTextEngineWithProperties(SDL_PropertiesID props)
+{
+    SDL_GPUDevice *device = SDL_GetPointerProperty(props, TTF_PROP_GPU_TEXT_ENGINE_DEVICE, NULL);
     if (!device) {
-        SDL_InvalidParamError("device");
+        SDL_SetError("Failed to create GPU text engine: Invalid device.");
         return NULL;
     }
 
@@ -949,10 +963,16 @@ TTF_TextEngine *TTF_CreateGPUTextEngine(SDL_GPUDevice *device)
         return NULL;
     }
 
+    int atlas_texture_size = (int)SDL_GetNumberProperty(props, TTF_PROP_GPU_TEXT_ENGINE_ATLAS_TEXTURE_SIZE, 1024);
+    if (atlas_texture_size <= 0) {
+        SDL_SetError("Failed to create GPU text engine: Invalid texture atlas size.");
+        return NULL;
+    }
+
     SDL_INIT_INTERFACE(engine);
     engine->CreateText = CreateText;
     engine->DestroyText = DestroyText;
-    engine->userdata = CreateEngineData(device);
+    engine->userdata = CreateEngineData(device, atlas_texture_size);
     if (!engine->userdata) {
         TTF_DestroyGPUTextEngine(engine);
         return NULL;
diff --git a/src/SDL_renderer_textengine.c b/src/SDL_renderer_textengine.c
index 042e7592..2de15fa5 100644
--- a/src/SDL_renderer_textengine.c
+++ b/src/SDL_renderer_textengine.c
@@ -23,8 +23,6 @@
 #include "SDL_hashtable.h"
 #include "SDL_hashtable_ttf.h"
 
-#define ATLAS_TEXTURE_SIZE  1024
-
 #define STB_RECT_PACK_IMPLEMENTATION
 #define STBRP_STATIC
 #define STBRP_SORT SDL_qsort
@@ -85,6 +83,7 @@ typedef struct TTF_RendererTextEngineData
     SDL_Renderer *renderer;
     SDL_HashTable *fonts;
     AtlasTexture *atlas;
+    int atlas_texture_size;
 } TTF_RendererTextEngineData;
 
 
@@ -183,27 +182,27 @@ static void DestroyAtlas(AtlasTexture *atlas)
     SDL_free(atlas);
 }
 
-static AtlasTexture *CreateAtlas(SDL_Renderer *renderer)
+static AtlasTexture *CreateAtlas(SDL_Renderer *renderer, int atlas_texture_size)
 {
     AtlasTexture *atlas = (AtlasTexture *)SDL_calloc(1, sizeof(*atlas));
     if (!atlas) {
         return NULL;
     }
 
-    atlas->texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, ATLAS_TEXTURE_SIZE, ATLAS_TEXTURE_SIZE);
+    atlas->texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, atlas_texture_size, atlas_texture_size);
     if (!atlas->texture) {
         DestroyAtlas(atlas);
         return NULL;
     }
     SDL_SetTextureScaleMode(atlas->texture, SDL_SCALEMODE_NEAREST);
 
-    int num_nodes = ATLAS_TEXTURE_SIZE / 4;
+    int num_nodes = atlas_texture_size / 4;
     atlas->packing_nodes = (stbrp_node *)SDL_calloc(num_nodes, sizeof(*atlas->packing_nodes));
     if (!atlas->packing_nodes) {
         DestroyAtlas(atlas);
         return NULL;
     }
-    stbrp_init_target(&atlas->packer, ATLAS_TEXTURE_SIZE, ATLAS_TEXTURE_SIZE, atlas->packing_nodes, num_nodes);
+    stbrp_init_target(&atlas->packer, atlas_texture_size, atlas_texture_size, atlas->packing_nodes, num_nodes);
     stbrp_setup_heuristic(&atlas->packer, STBRP_HEURISTIC_Skyline_default);
 
     return atlas;
@@ -241,7 +240,7 @@ static void ReleaseGlyph(AtlasGlyph *glyph)
     }
 }
 
-static AtlasGlyph *CreateGlyph(AtlasTexture *atlas, const stbrp_rect *area)
+static AtlasGlyph *CreateGlyph(AtlasTexture *atlas, int atlas_texture_size, const stbrp_rect *area)
 {
     AtlasGlyph *glyph = (AtlasGlyph *)SDL_calloc(1, sizeof(*glyph));
     if (!glyph) {
@@ -255,10 +254,10 @@ static AtlasGlyph *CreateGlyph(AtlasTexture *atlas, const stbrp_rect *area)
     glyph->rect.w = area->w;
     glyph->rect.h = area->h;
 
-    const float minu = (float)area->x / ATLAS_TEXTURE_SIZE;
-    const float minv = (float)area->y / ATLAS_TEXTURE_SIZE;
-    const float maxu = (float)(area->x + area->w) / ATLAS_TEXTURE_SIZE;
-    const float maxv = (float)(area->y + area->h) / ATLAS_TEXTURE_SIZE;
+    const float minu = (float)area->x / atlas_texture_size;
+    const float minv = (float)area->y / atlas_texture_size;
+    const float maxu = (float)(area->x + area->w) / atlas_texture_size;
+    const float maxv = (float)(area->y + area->h) / atlas_texture_size;
     glyph->texcoords[0] = minu;
     glyph->texcoords[1] = minv;
     glyph->texcoords[2] = maxu;
@@ -374,7 +373,7 @@ static bool ResolveMissingGlyphs(TTF_RendererTextEngineData *enginedata, AtlasTe
             continue;
         }
 
-        AtlasGlyph *glyph = CreateGlyph(atlas, &missing[i]);
+        AtlasGlyph *glyph = CreateGlyph(atlas, enginedata->atlas_texture_size, &missing[i]);
         if (!glyph) {
             return false;
         }
@@ -408,7 +407,7 @@ static bool ResolveMissingGlyphs(TTF_RendererTextEngineData *enginedata, AtlasTe
     }
 
     if (!atlas->next) {
-        atlas->next = CreateAtlas(enginedata->renderer);
+        atlas->next = CreateAtlas(enginedata->renderer, enginedata->atlas_texture_size);
         if (!atlas->next) {
             return false;
         }
@@ -422,6 +421,7 @@ static bool CreateMissingGlyphs(TTF_RendererTextEngineData *enginedata, TTF_Rend
     SDL_Surface **surfaces = NULL;
     SDL_HashTable *checked = NULL;
     bool result = false;
+    int atlas_texture_size = enginedata->atlas_texture_size;
 
     // Build a list of missing glyphs
     missing = (stbrp_rect *)SDL_calloc(num_missing, sizeof(*missing));
@@ -456,10 +456,10 @@ static bool CreateMissingGlyphs(TTF_RendererTextEngineData *enginedata, TTF_Rend
             if (!surfaces[i]) {
                 goto done;
             }
-            if (surfaces[i]->w > ATLAS_TEXTURE_SIZE || surfaces[i]->h > ATLAS_TEXTURE_SIZE) {
+            if (surfaces[i]->w > atlas_texture_size || surfaces[i]->h > atlas_texture_size) {
                 SDL_SetError("Glyph surface %dx%d larger than atlas texture %dx%d",
                     surfaces[i]->w, surfaces[i]->h,
-                    ATLAS_TEXTURE_SIZE, ATLAS_TEXTURE_SIZE);
+                    atlas_texture_size, atlas_texture_size);
                 goto done;
             }
 
@@ -476,7 +476,7 @@ static bool CreateMissingGlyphs(TTF_RendererTextEngineData *enginedata, TTF_Rend
 
     // Create the texture atlas if necessary
     if (!enginedata->atlas) {
-        enginedata->atlas = CreateAtlas(enginedata->renderer);
+        enginedata->atlas = CreateAtlas(enginedata->renderer, atlas_texture_size);
         if (!enginedata->atlas) {
             goto done;
         }
@@ -778,13 +778,14 @@ static void NukeFontData(const void *key, const void *value, void *unused)
     DestroyFontData(data);
 }
 
-static TTF_RendererTextEngineData *CreateEngineData(SDL_Renderer *renderer)
+static TTF_RendererTextEngineData *CreateEngineData(SDL_Renderer *renderer, int atlas_texture_size)
 {
     TTF_RendererTextEngineData *data = (TTF_RendererTextEngineData *)SDL_calloc(1, sizeof(*data));
     if (!data) {
         return NULL;
     }
     data->renderer = renderer;
+    data->atlas_texture_size = atlas_texture_size;
 
     data->fonts = SDL_CreateHashTable(NULL, 4, SDL_HashPointer, SDL_KeyMatchPointer, NukeFontData, false, false);
     if (!data->fonts) {
@@ -840,8 +841,21 @@ static void SDLCALL DestroyText(void *userdata, TTF_Text *text)
 
 TTF_TextEngine *TTF_CreateRendererTextEngine(SDL_Renderer *renderer)
 {
+    SDL_PropertiesID props = SDL_CreateProperties();
+    if (props == 0) {
+        SDL_SetError("Failed to create renderer text engine.");
+        return NULL;
+    }
+    SDL_SetPointerProperty(props, TTF_PROP_RENDERER_TEXT_ENGINE_RENDERER, renderer);
+
+    return TTF_CreateRendererTextEngineWithProperties(props);
+}
+
+TTF_TextEngine *TTF_CreateRendererTextEngineWithProperties(SDL_PropertiesID props)
+{
+    SDL_Renderer *renderer = SDL_GetPointerProperty(props, TTF_PROP_RENDERER_TEXT_ENGINE_RENDERER, NULL);
     if (!renderer) {
-        SDL_InvalidParamError("renderer");
+        SDL_SetError("Failed to create renderer text engine: Invalid renderer.");
         return NULL;
     }
 
@@ -850,10 +864,16 @@ TTF_TextEngine *TTF_CreateRendererTextEngine(SDL_Renderer *renderer)
         return NULL;
     }
 
+    int atlas_texture_size = (int)SDL_GetNumberProperty(props, TTF_PROP_RENDERER_TEXT_ENGINE_ATLAS_TEXTURE_SIZE, 1024);
+    if (atlas_texture_size <= 0) {
+        SDL_SetError("Failed to create renderer text engine: Invalid texture atlas size.");
+        return NULL;
+    }
+
     SDL_INIT_INTERFACE(engine);
     engine->CreateText = CreateText;
     engine->DestroyText = DestroyText;
-    engine->userdata = CreateEngineData(renderer);
+    engine->userdata = CreateEngineData(renderer, atlas_texture_size);
     if (!engine->userdata) {
         TTF_DestroyRendererTextEngine(engine);
         return NULL;
diff --git a/src/SDL_ttf.sym b/src/SDL_ttf.sym
index bfe5fae3..ae56f1c1 100644
--- a/src/SDL_ttf.sym
+++ b/src/SDL_ttf.sym
@@ -5,7 +5,9 @@ SDL3_ttf_0.0.0 {
     TTF_ClearFallbackFonts;
     TTF_CloseFont;
     TTF_CreateGPUTextEngine;
+    TTF_CreateGPUTextEngineWithProperties;
     TTF_CreateRendererTextEngine;
+    TTF_CreateRendererTextEngineWithProperties;
     TTF_CreateSurfaceTextEngine;
     TTF_CreateText;
     TTF_DeleteTextString;