SDL_ttf: Add procedural edge AA support for solid fill in GPU text engine

From e07148f7c6da01ee694acd73a1cfbadde3ebb854 Mon Sep 17 00:00:00 2001
From: Ivan Vecera <[EMAIL REDACTED]>
Date: Thu, 18 Jun 2026 09:33:38 +0200
Subject: [PATCH] Add procedural edge AA support for solid fill in GPU text
 engine

Populate UV coordinates for solid fill sequences with normalized [0,1]
rectangle coordinates instead of leaving them NULL. This enables
shader-based procedural anti-aliasing for underline and strikethrough
rendering. Fill rects shorter than 3px are padded symmetrically to
ensure the shader has enough geometry for smooth edge transitions.
---
 include/SDL3_ttf/SDL_ttf.h |  2 +-
 src/SDL_gpu_textengine.c   | 33 +++++++++++++++++++++++----------
 2 files changed, 24 insertions(+), 11 deletions(-)

diff --git a/include/SDL3_ttf/SDL_ttf.h b/include/SDL3_ttf/SDL_ttf.h
index 9fb5cd12..0c4aa259 100644
--- a/include/SDL3_ttf/SDL_ttf.h
+++ b/include/SDL3_ttf/SDL_ttf.h
@@ -1979,7 +1979,7 @@ typedef struct TTF_GPUAtlasDrawSequence
 {
     SDL_GPUTexture *atlas_texture;          /**< Texture atlas that stores the glyphs, or NULL for solid fill */
     SDL_FPoint *xy;                         /**< An array of vertex positions */
-    SDL_FPoint *uv;                         /**< An array of normalized texture coordinates for each vertex, or NULL for solid fill */
+    SDL_FPoint *uv;                         /**< An array of normalized texture coordinates for each vertex */
     int num_vertices;                       /**< Number of vertices */
     int *indices;                           /**< An array of indices into the 'vertices' arrays */
     int num_indices;                        /**< Number of indices */
diff --git a/src/SDL_gpu_textengine.c b/src/SDL_gpu_textengine.c
index b8014b52..9f012be5 100644
--- a/src/SDL_gpu_textengine.c
+++ b/src/SDL_gpu_textengine.c
@@ -651,21 +651,27 @@ static AtlasDrawSequence *CreateDrawSequence(TTF_DrawOperation *ops, int num_ops
     sequence->num_vertices = count * 4;
     sequence->num_indices = count * 6;
 
-    if (texture) {
-        AtlasGlyph *glyph;
-
-        sequence->uv = (SDL_FPoint *)SDL_malloc(count * sizeof(glyph->texcoords));
-        if (!sequence->uv) {
-            DestroyDrawSequence(sequence);
-            return NULL;
-        }
+    sequence->uv = (SDL_FPoint *)SDL_malloc(count * 4 * sizeof(*sequence->uv));
+    if (!sequence->uv) {
+        DestroyDrawSequence(sequence);
+        return NULL;
+    }
 
-        float *uv = (float *)sequence->uv;
+    float *uv = (float *)sequence->uv;
+    if (texture) {
         for (int i = 0; i < count; ++i) {
-            glyph = (AtlasGlyph *)ops[i].copy.reserved;
+            AtlasGlyph *glyph = (AtlasGlyph *)ops[i].copy.reserved;
             SDL_memcpy(uv, glyph->texcoords, sizeof(glyph->texcoords));
             uv += SDL_arraysize(glyph->texcoords);
         }
+    } else {
+        for (int i = 0; i < count; ++i) {
+            /* Normalized [0,1] rectangle coords for procedural edge AA */
+            *uv++ = 0.0f; *uv++ = 0.0f;
+            *uv++ = 1.0f; *uv++ = 0.0f;
+            *uv++ = 1.0f; *uv++ = 1.0f;
+            *uv++ = 0.0f; *uv++ = 1.0f;
+        }
     }
 
     sequence->xy = (SDL_FPoint *)SDL_malloc(count * 4 * sizeof(*sequence->xy));
@@ -693,6 +699,13 @@ static AtlasDrawSequence *CreateDrawSequence(TTF_DrawOperation *ops, int num_ops
         float miny = (float)dst->y;
         float maxy = (float)(dst->y + dst->h);
 
+        /* Ensure fill rects are at least 3px tall for shader-based edge AA */
+        if (op->cmd == TTF_DRAW_COMMAND_FILL && dst->h < 3) {
+            float pad = (3.0f - dst->h) / 2.0f;
+            miny -= pad;
+            maxy += pad;
+        }
+
         // In the GPU API postive y-axis is upwards so the signs of the y-coords is reversed
         *xy++ =  minx;
         *xy++ = -miny;