SDL_ttf: Fixed SDF text layout

From 2ac93e2f07764b58d3cca929c650158361080597 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Tue, 28 Jan 2025 13:27:01 -0800
Subject: [PATCH] Fixed SDF text layout

We're also passing through the raw SDF values now so shaders can be used to render them correctly.
---
 include/SDL3_ttf/SDL_ttf.h |   5 +-
 src/SDL_gpu_textengine.c   |   8 +-
 src/SDL_ttf.c              | 190 ++++++++++++++++++-------------------
 3 files changed, 101 insertions(+), 102 deletions(-)

diff --git a/include/SDL3_ttf/SDL_ttf.h b/include/SDL3_ttf/SDL_ttf.h
index ea7f43d0..e614f41e 100644
--- a/include/SDL3_ttf/SDL_ttf.h
+++ b/include/SDL3_ttf/SDL_ttf.h
@@ -601,8 +601,9 @@ extern SDL_DECLSPEC TTF_HintingFlags SDLCALL TTF_GetFontHinting(const TTF_Font *
 /**
  * Enable Signed Distance Field rendering for a font.
  *
- * This works with the Blended APIs. SDF is a technique that
- * helps fonts look sharp even when scaling and rotating.
+ * SDF is a technique that helps fonts look sharp even when scaling and rotating, and requires special shader support for display.
+ *
+ * This works with Blended APIs, and generates the raw signed distance values in the alpha channel of the resulting texture.
  *
  * This updates any TTF_Text objects using this font, and clears already-generated glyphs, if any, from the cache.
  *
diff --git a/src/SDL_gpu_textengine.c b/src/SDL_gpu_textengine.c
index fd7142f3..c3486fcd 100644
--- a/src/SDL_gpu_textengine.c
+++ b/src/SDL_gpu_textengine.c
@@ -381,10 +381,10 @@ static bool UpdateGPUTexture(SDL_GPUDevice *device, SDL_GPUTexture *texture,
 
 static bool UpdateGlyph(SDL_GPUDevice *device, AtlasGlyph *glyph, SDL_Surface *surface)
 {
-    if (glyph->rect.w > 0 && glyph->rect.h > 0) {
-        /* FIXME: We should update the whole texture at once or at least cache the transfer buffers */
-        UpdateGPUTexture(device, glyph->atlas->texture, &glyph->rect, surface->pixels, surface->pitch);
-    }
+    SDL_assert(glyph->rect.w > 0 && glyph->rect.h > 0);
+
+    /* FIXME: We should update the whole texture at once or at least cache the transfer buffers */
+    UpdateGPUTexture(device, glyph->atlas->texture, &glyph->rect, surface->pixels, surface->pitch);
     return true;
 }
 
diff --git a/src/SDL_ttf.c b/src/SDL_ttf.c
index 0abbbf1a..95bf11cc 100644
--- a/src/SDL_ttf.c
+++ b/src/SDL_ttf.c
@@ -546,18 +546,10 @@ static void BG_Blended_Opaque_SDF(const TTF_Image *image, Uint32 *destination, S
     Uint32       width  = image->width;
     Uint32       height = image->rows;
 
-    Uint32 s;
-    Uint32 d;
-
     while (height--) {
         /* *INDENT-OFF* */
         DUFFS_LOOP4(
-            d = *dst;
-            s = ((Uint32)*src++) << 24;
-            if (s > d) {
-                *dst = s;
-            }
-            dst++;
+            *dst++ = ((Uint32)*src++) << 24;
         , width);
         /* *INDENT-ON* */
         src += srcskip;
@@ -566,6 +558,8 @@ static void BG_Blended_Opaque_SDF(const TTF_Image *image, Uint32 *destination, S
 }
 
 // Blended non-opaque SDF
+// Note: This doesn't make sense when we're outputting raw SDF values.
+//       We'll just copy the alpha channel as-is for now.
 static void BG_Blended_SDF(const TTF_Image *image, Uint32 *destination, Sint32 srcskip, Uint32 dstskip, Uint8 fg_alpha)
 {
     const Uint8 *src    = image->buffer;
@@ -573,20 +567,11 @@ static void BG_Blended_SDF(const TTF_Image *image, Uint32 *destination, Sint32 s
     Uint32       width  = image->width;
     Uint32       height = image->rows;
 
-    Uint32 s;
-    Uint32 d;
-
-    Uint32 tmp;
+    (void)fg_alpha;
     while (height--) {
         /* *INDENT-OFF* */
         DUFFS_LOOP4(
-            d = *dst;
-            tmp = fg_alpha * (*src++);
-            s = DIVIDE_BY_255(tmp) << 24;
-            if (s > d) {
-                *dst = s;
-            }
-            dst++;
+            *dst++ = ((Uint32)*src++) << 24;
         , width);
         /* *INDENT-ON* */
         src += srcskip;
@@ -1496,27 +1481,29 @@ static bool Render_Line_TextEngine(TTF_Font *font, TTF_Direction direction, int
         x = xstart + FT_FLOOR(x) + glyph->sz_left;
         y = ystart + FT_FLOOR(y) - glyph->sz_top;
 
-        // Make sure glyph is inside text area
-        above_w = x + glyph_width - width;
-        above_h = y + glyph_rows  - height;
+        if (!glyph_font->render_sdf) {
+            // Make sure glyph is inside text area
+            above_w = x + glyph_width - width;
+            above_h = y + glyph_rows  - height;
 
-        if (x < 0) {
-            int tmp = -x;
-            x = 0;
-            glyph_x += tmp;
-            glyph_width -= tmp;
-        }
-        if (above_w > 0) {
-            glyph_width -= above_w;
-        }
-        if (y < 0) {
-            int tmp = -y;
-            y = 0;
-            glyph_y += tmp;
-            glyph_rows -= tmp;
-        }
-        if (above_h > 0) {
-            glyph_rows -= above_h;
+            if (x < 0) {
+                int tmp = -x;
+                x = 0;
+                glyph_x += tmp;
+                glyph_width -= tmp;
+            }
+            if (above_w > 0) {
+                glyph_width -= above_w;
+            }
+            if (y < 0) {
+                int tmp = -y;
+                y = 0;
+                glyph_y += tmp;
+                glyph_rows -= tmp;
+            }
+            if (above_h > 0) {
+                glyph_rows -= above_h;
+            }
         }
 
         if (glyph_width > 0 && glyph_rows > 0) {
@@ -1533,6 +1520,12 @@ static bool Render_Line_TextEngine(TTF_Font *font, TTF_Direction direction, int
             op->copy.dst.y = y;
             op->copy.dst.w = op->copy.src.w;
             op->copy.dst.h = op->copy.src.h;
+            if (glyph_font->render_sdf) {
+                op->copy.dst.x -= DEFAULT_SDF_SPREAD;
+                op->copy.dst.y -= DEFAULT_SDF_SPREAD;
+                op->copy.dst.w -= DEFAULT_SDF_SPREAD;
+                op->copy.dst.h -= DEFAULT_SDF_SPREAD;
+            }
             if (FT_HAS_SVG(glyph_font->face)) {
                 op->copy.flags = TTF_COPY_OPERATION_IMAGE;
             }
@@ -2531,7 +2524,6 @@ static bool Load_Glyph(TTF_Font *font, c_glyph *cached, int want, int translatio
 
         // Adjust for SDF
         if (font->render_sdf) {
-            // Default 'spread' property
             cached->sz_width += 2 * DEFAULT_SDF_SPREAD;
             cached->sz_rows  += 2 * DEFAULT_SDF_SPREAD;
         }
@@ -2837,30 +2829,9 @@ static bool Load_Glyph(TTF_Font *font, c_glyph *cached, int want, int translatio
                         *dstp++ = r;
                         *dstp++ = alpha;
                     }
+
                 } else {
-#if TTF_USE_SDF
-                    if (ft_render_mode != FT_RENDER_MODE_SDF) {
-                        SDL_memcpy(dstp, srcp, src->width);
-                    } else {
-                        unsigned int x;
-                        for (x = 0; x < src->width; x++) {
-                            Uint8 s = srcp[x];
-                            Uint8 d;
-                            if (s < 128) {
-                                d = 256 - (128 - s) * 2;
-                            } else {
-                                d = 255;
-                                /* some glitch ?
-                                if (s == 255) {
-                                    d = 0;
-                                }*/
-                            }
-                            dstp[x] = d;
-                        }
-                    }
-#else
                     SDL_memcpy(dstp, srcp, src->width);
-#endif
                 }
             }
         }
@@ -3119,8 +3090,18 @@ SDL_Surface *TTF_GetGlyphImageForIndex(TTF_Font *font, Uint32 glyph_index)
 
     TTF_CHECK_FONT(font, NULL);
 
-    if (!Find_GlyphByIndex(font, glyph_index, PIXMAP, 0, 0, NULL, &image)) {
-        return NULL;
+    if (font->render_sdf) {
+        if (!Find_GlyphByIndex(font, glyph_index, COLOR, 0, 0, NULL, &image)) {
+            return NULL;
+        }
+    } else {
+        if (!Find_GlyphByIndex(font, glyph_index, PIXMAP, 0, 0, NULL, &image)) {
+            return NULL;
+        }
+    }
+
+    if (image->width == 0 || image->rows == 0) {
+        return SDL_CreateSurface(1, 1, SDL_PIXELFORMAT_ARGB8888);
     }
 
     surface = SDL_CreateSurface(image->width, image->rows, SDL_PIXELFORMAT_ARGB8888);
@@ -3584,12 +3565,13 @@ static GlyphPositions *GetCachedGlyphPositions(TTF_Font *font, const char *text,
     return font->positions;
 }
 
-static bool TTF_Size_Internal(TTF_Font *font, const char *text, size_t length, TTF_Direction direction, Uint32 script, int *w, int *h, int *xstart, int *ystart, bool measure_width, int max_width, int *measured_width, size_t *measured_length)
+static bool TTF_Size_Internal(TTF_Font *font, const char *text, size_t length, TTF_Direction direction, Uint32 script, int *w, int *h, int *xstart, int *ystart, bool measure_width, int max_width, int *measured_width, size_t *measured_length, bool include_spread)
 {
     int x = 0;
     int pos_x, pos_y;
-    int minx = 0, maxx = 0;
-    int miny = 0, maxy = 0;
+    int minx, maxx;
+    int miny, maxy;
+    int spread_adjustment;
 
     if (w) {
         *w = 0;
@@ -3612,7 +3594,11 @@ static bool TTF_Size_Internal(TTF_Font *font, const char *text, size_t length, T
         *measured_length = length;
     }
 
-    maxy = font->height;
+    if (font->render_sdf && !include_spread) {
+        spread_adjustment = DEFAULT_SDF_SPREAD;
+    } else {
+        spread_adjustment = 0;
+    }
 
     GlyphPositions *positions = GetCachedGlyphPositions(font, text, length, direction, script);
     if (!positions) {
@@ -3620,6 +3606,11 @@ static bool TTF_Size_Internal(TTF_Font *font, const char *text, size_t length, T
     }
 
     if (positions->len > 0) {
+        minx = INT_MAX;
+        maxx = INT_MIN;
+        miny = INT_MAX;
+        maxy = font->height;
+
         if (positions->pos[0].offset == 0) {
             // Left to right layout
             for (int i = 0; i < positions->len; ++i) {
@@ -3627,13 +3618,13 @@ static bool TTF_Size_Internal(TTF_Font *font, const char *text, size_t length, T
                 c_glyph *glyph = pos->glyph;
 
                 // Compute provisional global bounding box
-                pos_x = FT_FLOOR(pos->x) + glyph->sz_left;
-                pos_y = FT_FLOOR(pos->y) - glyph->sz_top;
+                pos_x = FT_FLOOR(pos->x) + glyph->sz_left + spread_adjustment;
+                pos_y = FT_FLOOR(pos->y) - glyph->sz_top + spread_adjustment;
 
                 minx = SDL_min(minx, pos_x);
-                maxx = SDL_max(maxx, pos_x + glyph->sz_width);
+                maxx = SDL_max(maxx, pos_x + glyph->sz_width - 2 * spread_adjustment);
                 miny = SDL_min(miny, pos_y);
-                maxy = SDL_max(maxy, pos_y + glyph->sz_rows);
+                maxy = SDL_max(maxy, pos_y + glyph->sz_rows - 2 * spread_adjustment);
 
                 x += pos->x_advance;
 #if !TTF_USE_HARFBUZZ
@@ -3666,13 +3657,13 @@ static bool TTF_Size_Internal(TTF_Font *font, const char *text, size_t length, T
                 c_glyph *glyph = pos->glyph;
 
                 // Compute provisional global bounding box
-                pos_x = FT_FLOOR(pos->x) + glyph->sz_left;
-                pos_y = FT_FLOOR(pos->y) - glyph->sz_top;
+                pos_x = FT_FLOOR(pos->x) + glyph->sz_left + spread_adjustment;
+                pos_y = FT_FLOOR(pos->y) - glyph->sz_top + spread_adjustment;
 
                 minx = SDL_min(minx, pos_x);
-                maxx = SDL_max(maxx, pos_x + glyph->sz_width);
+                maxx = SDL_max(maxx, pos_x + glyph->sz_width - 2 * spread_adjustment);
                 miny = SDL_min(miny, pos_y);
-                maxy = SDL_max(maxy, pos_y + glyph->sz_rows);
+                maxy = SDL_max(maxy, pos_y + glyph->sz_rows - 2 * spread_adjustment);
 
                 // Measurement mode
                 if (measure_width) {
@@ -3701,6 +3692,11 @@ static bool TTF_Size_Internal(TTF_Font *font, const char *text, size_t length, T
                 minx = 0;
             }
         }
+    } else {
+        minx = 0;
+        maxx = 0;
+        miny = 0;
+        maxy = font->height;
     }
 
     // Allows to render a string with only one space (bug 4344).
@@ -3711,8 +3707,8 @@ static bool TTF_Size_Internal(TTF_Font *font, const char *text, size_t length, T
     if (xstart) {
         *xstart = (minx < 0) ? -minx : 0;
         *xstart += font->outline;
-        if (font->render_sdf) {
-            *xstart += DEFAULT_SDF_SPREAD; // Default 'spread' property
+        if (font->render_sdf && include_spread) {
+            *xstart += DEFAULT_SDF_SPREAD;
         }
     }
 
@@ -3720,8 +3716,8 @@ static bool TTF_Size_Internal(TTF_Font *font, const char *text, size_t length, T
     if (ystart) {
         *ystart = (miny < 0) ? -miny : 0;
         *ystart += font->outline;
-        if (font->render_sdf) {
-            *ystart += DEFAULT_SDF_SPREAD; // Default 'spread' property
+        if (font->render_sdf && include_spread) {
+            *ystart += DEFAULT_SDF_SPREAD;
         }
     }
 
@@ -3744,7 +3740,7 @@ bool TTF_GetStringSize(TTF_Font *font, const char *text, size_t length, int *w,
     if (!length && text) {
         length = SDL_strlen(text);
     }
-    return TTF_Size_Internal(font, text, length, font->direction, font->script, w, h, NULL, NULL, NO_MEASUREMENT);
+    return TTF_Size_Internal(font, text, length, font->direction, font->script, w, h, NULL, NULL, NO_MEASUREMENT, true);
 }
 
 bool TTF_MeasureString(TTF_Font *font, const char *text, size_t length, int max_width, int *measured_width, size_t *measured_length)
@@ -3752,7 +3748,7 @@ bool TTF_MeasureString(TTF_Font *font, const char *text, size_t length, int max_
     if (!length && text) {
         length = SDL_strlen(text);
     }
-    return TTF_Size_Internal(font, text, length, font->direction, font->script, NULL, NULL, NULL, NULL, true, max_width, measured_width, measured_length);
+    return TTF_Size_Internal(font, text, length, font->direction, font->script, NULL, NULL, NULL, NULL, true, max_width, measured_width, measured_length, true);
 }
 
 static SDL_Surface* TTF_Render_Internal(TTF_Font *font, const char *text, size_t length, SDL_Color fg, SDL_Color bg, const render_mode_t render_mode)
@@ -3785,7 +3781,7 @@ static SDL_Surface* TTF_Render_Internal(TTF_Font *font, const char *text, size_t
 #endif
 
     // Get the dimensions of the text surface
-    if (!TTF_Size_Internal(font, text, length, font->direction, font->script, &width, &height, &xstart, &ystart, NO_MEASUREMENT) || !width) {
+    if (!TTF_Size_Internal(font, text, length, font->direction, font->script, &width, &height, &xstart, &ystart, NO_MEASUREMENT, true) || !width) {
         SDL_SetError("Text has zero width");
         goto failure;
     }
@@ -3903,7 +3899,7 @@ static bool CharacterIsNewLine(Uint32 c)
     return false;
 }
 
-static bool GetWrappedLines(TTF_Font *font, const char *text, size_t length, TTF_Direction direction, Uint32 script, int xoffset, int wrap_width, bool trim_whitespace, TTF_Line **lines, int *num_lines, int *w, int *h)
+static bool GetWrappedLines(TTF_Font *font, const char *text, size_t length, TTF_Direction direction, Uint32 script, int xoffset, int wrap_width, bool trim_whitespace, TTF_Line **lines, int *num_lines, int *w, int *h, bool include_spread)
 {
     int width, height;
     int i, numLines = 0, rowHeight;
@@ -3929,7 +3925,7 @@ static bool GetWrappedLines(TTF_Font *font, const char *text, size_t length, TTF
     }
 
     // Get the dimensions of the text surface
-    if (!TTF_Size_Internal(font, text, length, direction, script, &width, &height, NULL, NULL, NO_MEASUREMENT) || !width) {
+    if (!TTF_Size_Internal(font, text, length, direction, script, &width, &height, NULL, NULL, NO_MEASUREMENT, include_spread) || !width) {
         return SDL_SetError("Text has zero width");
     }
 
@@ -3984,7 +3980,7 @@ static bool GetWrappedLines(TTF_Font *font, const char *text, size_t length, TTF
                 max_width = SDL_max(max_width - xoffset, 1);
             }
             size_t max_length = 0;
-            if (!TTF_Size_Internal(font, spot, left, direction, script, NULL, NULL, NULL, NULL, true, max_width, NULL, &max_length)) {
+            if (!TTF_Size_Internal(font, spot, left, direction, script, NULL, NULL, NULL, NULL, true, max_width, NULL, &max_length, include_spread)) {
                 SDL_SetError("Error measure text");
                 goto done;
             }
@@ -4067,7 +4063,7 @@ static bool GetWrappedLines(TTF_Font *font, const char *text, size_t length, TTF
             for (i = 0; i < numLines; i++) {
                 int w_tmp, h_tmp;
 
-                if (TTF_Size_Internal(font, strLines[i].text, strLines[i].length, font->direction, font->script, &w_tmp, &h_tmp, NULL, NULL, NO_MEASUREMENT)) {
+                if (TTF_Size_Internal(font, strLines[i].text, strLines[i].length, font->direction, font->script, &w_tmp, &h_tmp, NULL, NULL, NO_MEASUREMENT, include_spread)) {
                     width = SDL_max(w_tmp, width);
                 }
             }
@@ -4110,7 +4106,7 @@ static bool GetWrappedLines(TTF_Font *font, const char *text, size_t length, TTF
 
 bool TTF_GetStringSizeWrapped(TTF_Font *font, const char *text, size_t length, int wrap_width, int *w, int *h)
 {
-    return GetWrappedLines(font, text, length, font->direction, font->script, 0, wrap_width, true, NULL, NULL, w, h);
+    return GetWrappedLines(font, text, length, font->direction, font->script, 0, wrap_width, true, NULL, NULL, w, h, true);
 }
 
 static SDL_Surface* TTF_Render_Wrapped_Internal(TTF_Font *font, const char *text, size_t length, SDL_Color fg, SDL_Color bg, int wrap_width, const render_mode_t render_mode)
@@ -4121,7 +4117,7 @@ static SDL_Surface* TTF_Render_Wrapped_Internal(TTF_Font *font, const char *text
     int i, numLines = 0;
     TTF_Line *strLines = NULL;
 
-    if (!GetWrappedLines(font, text, length, font->direction, font->script, 0, wrap_width, true, &strLines, &numLines, &width, &height)) {
+    if (!GetWrappedLines(font, text, length, font->direction, font->script, 0, wrap_width, true, &strLines, &numLines, &width, &height, true)) {
         return NULL;
     }
 
@@ -4163,7 +4159,7 @@ static SDL_Surface* TTF_Render_Wrapped_Internal(TTF_Font *font, const char *text
         int xstart, ystart, line_width, xoffset;
 
         // Initialize xstart, ystart and compute positions
-        if (!TTF_Size_Internal(font, strLines[i].text, strLines[i].length, font->direction, font->script, &line_width, NULL, &xstart, &ystart, NO_MEASUREMENT)) {
+        if (!TTF_Size_Internal(font, strLines[i].text, strLines[i].length, font->direction, font->script, &line_width, NULL, &xstart, &ystart, NO_MEASUREMENT, true)) {
             goto failure;
         }
 
@@ -4411,7 +4407,7 @@ static bool LayoutText(TTF_Text *text)
     TTF_Direction direction = TTF_GetTextDirection(text);
     Uint32 script = TTF_GetTextScript(text);
 
-    if (!GetWrappedLines(font, text->text, length, direction, script, text->internal->x, wrap_width, trim_whitespace, &strLines, &numLines, &width, &height)) {
+    if (!GetWrappedLines(font, text->text, length, direction, script, text->internal->x, wrap_width, trim_whitespace, &strLines, &numLines, &width, &height, false)) {
         return true;
     }
     height += text->internal->y;
@@ -4452,7 +4448,7 @@ static bool LayoutText(TTF_Text *text)
         }
 
         // Initialize xstart, ystart and compute positions
-        if (!TTF_Size_Internal(font, strLines[i].text, strLines[i].length, direction, script, &line_width, NULL, &xstart, &ystart, NO_MEASUREMENT)) {
+        if (!TTF_Size_Internal(font, strLines[i].text, strLines[i].length, direction, script, &line_width, NULL, &xstart, &ystart, NO_MEASUREMENT, false)) {
             goto done;
         }
 
@@ -5693,9 +5689,11 @@ bool TTF_SetFontSDF(TTF_Font *font, bool enabled)
 {
     TTF_CHECK_FONT(font, false);
 #if TTF_USE_SDF
-    font->render_sdf = enabled;
-    Flush_Cache(font);
-    UpdateFontText(font, NULL);
+    if (font->render_sdf != enabled) {
+        font->render_sdf = enabled;
+        Flush_Cache(font);
+        UpdateFontText(font, NULL);
+    }
     return true;
 #else
     (void)enabled;