SDL_ttf: Added support for fallback fonts

From e4155d9a6ae77f3f487e852eadc251932ea4e08d Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Sun, 26 Jan 2025 09:46:42 -0800
Subject: [PATCH] Added support for fallback fonts

Fixes https://github.com/libsdl-org/SDL_ttf/issues/362
---
 CHANGES.txt                |   1 +
 examples/showfont.c        |  80 ++++++++++----
 include/SDL3_ttf/SDL_ttf.h |  59 ++++++++++
 src/SDL_ttf.c              | 219 ++++++++++++++++++++++++++++++-------
 src/SDL_ttf.sym            |   3 +
 5 files changed, 299 insertions(+), 63 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index a39ec1f5..2b61e537 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -35,3 +35,4 @@
     - TTF_GetNextTextSubString()
     - TTF_UpdateText()
     - TTF_DestroyText()
+ * Added TTF_AddFallbackFont() to allow combining fonts with distinct glyph support
diff --git a/examples/showfont.c b/examples/showfont.c
index 2cb95add..cb9990d6 100644
--- a/examples/showfont.c
+++ b/examples/showfont.c
@@ -37,12 +37,15 @@
 //#define DEFAULT_TEXT    "\xc5\xab\xcc\x80\x20\xe1\xba\x83\x20\x6e\xcc\x82\x20\x48\xcc\xa8\x20\x6f\xcd\x9c\x75"
 // Chinese text
 //#define DEFAULT_TEXT    "\xe5\xad\xa6\xe4\xb9\xa0\xe6\x9f\x90\xe8\xaf\xbe\xe7\xa8\x8b\xe5\xbf\x85\xe8\xaf\xbb\xe7\x9a\x84"
+// Mixed English, Chinese, and emoji text
+//#define DEFAULT_TEXT    "The quick brown fox\njumped over the \xe5\xad\xa6\xe4\xb9\xa0\xe6\x9f\x90\xe8\xaf\xbe\xe7\xa8\x8b\xe5\xbf\x85\xe8\xaf\xbb\xe7\x9a\x84 \xf0\x9f\x98\x89"
 #define WIDTH   640
 #define HEIGHT  480
+#define MAX_FALLBACKS 2
 
 
 #define TTF_SHOWFONT_USAGE \
-"Usage: %s [-textengine surface|renderer] [-solid] [-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] [-disable-editbox] <font>.ttf [ptsize] [text]\n"
+"Usage: %s [-textengine surface|renderer] [-solid] [-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] [-disable-editbox] [-fallback <font>.ttf>] <font>.ttf [ptsize] [text]\n"
 
 typedef enum
 {
@@ -264,13 +267,6 @@ static void HandleKeyDown(Scene *scene, SDL_Event *event)
     }
 }
 
-static void Cleanup(int exitcode)
-{
-    TTF_Quit();
-    SDL_Quit();
-    exit(exitcode);
-}
-
 int main(int argc, char *argv[])
 {
     char *argv0 = argv[0];
@@ -295,14 +291,29 @@ int main(int argc, char *argv[])
     bool editbox = true;
     bool dump = false;
     char *message, string[128];
+    int num_fallbacks = 0;
+    const char *fallback_font_files[MAX_FALLBACKS];
+    TTF_Font *fallback_fonts[MAX_FALLBACKS];
+    int result = 0;
 
     SDL_zero(scene);
     scene.textEngine = TextEngineRenderer;
 
+    SDL_zeroa(fallback_fonts);
+
     /* Default is black and white */
     forecol = &black;
     backcol = &white;
     for (i=1; argv[i] && argv[i][0] == '-'; ++i) {
+        if (SDL_strcmp(argv[i], "-fallback") == 0 && argv[i+1]) {
+            ++i;
+            if (num_fallbacks < MAX_FALLBACKS) {
+                fallback_font_files[num_fallbacks++] = argv[i];
+            } else {
+                SDL_Log("Too many fallback fonts (maximum = %d)\n", MAX_FALLBACKS);
+                return(1);
+            }
+        } else
         if (SDL_strcmp(argv[i], "-textengine") == 0 && argv[i+1]) {
             ++i;
             if (SDL_strcmp(argv[i], "surface") == 0) {
@@ -413,8 +424,8 @@ int main(int argc, char *argv[])
     /* Initialize the TTF library */
     if (!TTF_Init()) {
         SDL_Log("Couldn't initialize TTF: %s\n",SDL_GetError());
-        SDL_Quit();
-        return(2);
+        result = 2;
+        goto done;
     }
 
     /* Open the font file with the requested point size */
@@ -432,7 +443,8 @@ int main(int argc, char *argv[])
     if (font == NULL) {
         SDL_Log("Couldn't load %g pt font from %s: %s\n",
                     ptsize, argv[0], SDL_GetError());
-        Cleanup(2);
+        result = 2;
+        goto done;
     }
     TTF_SetFontStyle(font, renderstyle);
     TTF_SetFontOutline(font, outline);
@@ -441,6 +453,17 @@ int main(int argc, char *argv[])
     TTF_SetFontWrapAlignment(font, align);
     scene.font = font;
 
+    for (i = 0; i < num_fallbacks; ++i) {
+        fallback_fonts[i] = TTF_OpenFont(fallback_font_files[i], ptsize);
+        if (!fallback_fonts[i]) {
+            SDL_Log("Couldn't load %g pt font from %s: %s\n",
+                        ptsize, fallback_font_files[i], SDL_GetError());
+            result = 2;
+            goto done;
+        }
+        TTF_AddFallbackFont(font, fallback_fonts[i]);
+    }
+
     if(dump) {
         for(i = 48; i < 123; i++) {
             SDL_Surface* glyph = NULL;
@@ -454,20 +477,23 @@ int main(int argc, char *argv[])
             }
 
         }
-        Cleanup(0);
+        result = 0;
+        goto done;
     }
 
     /* Create a window */
     scene.window = SDL_CreateWindow("showfont demo", WIDTH, HEIGHT, 0);
     if (!scene.window) {
         SDL_Log("SDL_CreateWindow() failed: %s\n", SDL_GetError());
-        Cleanup(2);
+        result = 2;
+        goto done;
     }
     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);
+            result = 2;
+            goto done;
         }
         SDL_SetWindowSurfaceVSync(scene.window, 1);
 
@@ -480,7 +506,8 @@ int main(int argc, char *argv[])
     }
     if (!scene.renderer) {
         SDL_Log("SDL_CreateRenderer() failed: %s\n", SDL_GetError());
-        Cleanup(2);
+        result = 2;
+        goto done;
     }
 
     switch (scene.textEngine) {
@@ -488,14 +515,16 @@ int main(int argc, char *argv[])
         engine = TTF_CreateSurfaceTextEngine();
         if (!engine) {
             SDL_Log("Couldn't create surface text engine: %s\n", SDL_GetError());
-            Cleanup(2);
+            result = 2;
+            goto done;
         }
         break;
     case TextEngineRenderer:
         engine = TTF_CreateRendererTextEngine(scene.renderer);
         if (!engine) {
             SDL_Log("Couldn't create renderer text engine: %s\n", SDL_GetError());
-            Cleanup(2);
+            result = 2;
+            goto done;
         }
         break;
     default:
@@ -541,8 +570,8 @@ int main(int argc, char *argv[])
     }
     if (text == NULL) {
         SDL_Log("Couldn't render text: %s\n", SDL_GetError());
-        TTF_CloseFont(font);
-        Cleanup(2);
+        result = 2;
+        goto done;
     }
     scene.messageRect.x = (float)((WIDTH - text->w)/2);
     scene.messageRect.y = (float)((HEIGHT - text->h)/2);
@@ -603,9 +632,15 @@ int main(int argc, char *argv[])
         }
         DrawScene(&scene);
     }
+    result = 0;
+
+done:
     SDL_DestroySurface(text);
     EditBox_Destroy(scene.edit);
     TTF_DestroyText(scene.caption);
+    for (i = 0; i < num_fallbacks; ++i) {
+        TTF_CloseFont(fallback_fonts[i]);
+    }
     TTF_CloseFont(font);
     switch (scene.textEngine) {
     case TextEngineSurface:
@@ -618,8 +653,7 @@ int main(int argc, char *argv[])
         break;
     }
     SDL_DestroyTexture(scene.message);
-    Cleanup(0);
-
-    /* Not reached, but fixes compiler warnings */
-    return 0;
+    TTF_Quit();
+    SDL_Quit();
+    return result;
 }
diff --git a/include/SDL3_ttf/SDL_ttf.h b/include/SDL3_ttf/SDL_ttf.h
index f57e4309..1aad5ae9 100644
--- a/include/SDL3_ttf/SDL_ttf.h
+++ b/include/SDL3_ttf/SDL_ttf.h
@@ -273,6 +273,65 @@ extern SDL_DECLSPEC SDL_PropertiesID SDLCALL TTF_GetFontProperties(TTF_Font *fon
  */
 extern SDL_DECLSPEC Uint32 SDLCALL TTF_GetFontGeneration(TTF_Font *font);
 
+/**
+ * Add a fallback font.
+ *
+ * Add a font that will be used for glyphs that are not in the current font. The fallback font should have the same size and style as the current font.
+ *
+ * If there are multiple fallback fonts, they are used in the order added.
+ *
+ * This updates any TTF_Text objects using this font.
+ *
+ * \param font the font to modify.
+ * \param fallback the font to add as a fallback.
+ * \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 both
+ *               fonts.
+ *
+ * \since This function is available since SDL_ttf 3.0.0.
+ *
+ * \sa TTF_ClearFallbackFonts
+ * \sa TTF_RemoveFallbackFont
+ */
+extern SDL_DECLSPEC bool SDLCALL TTF_AddFallbackFont(TTF_Font *font, TTF_Font *fallback);
+
+/**
+ * Remove a fallback font.
+ *
+ * This updates any TTF_Text objects using this font.
+ *
+ * \param font the font to modify.
+ * \param fallback the font to remove as a fallback.
+ *
+ * \threadsafety This function should be called on the thread that created both
+ *               fonts.
+ *
+ * \since This function is available since SDL_ttf 3.0.0.
+ *
+ * \sa TTF_AddFallbackFont
+ * \sa TTF_ClearFallbackFonts
+ */
+extern SDL_DECLSPEC void SDLCALL TTF_RemoveFallbackFont(TTF_Font *font, TTF_Font *fallback);
+
+/**
+ * Remove all fallback fonts.
+ *
+ * This updates any TTF_Text objects using this font.
+ *
+ * \param font the font to modify.
+ *
+ * \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_AddFallbackFont
+ * \sa TTF_RemoveFallbackFont
+ */
+extern SDL_DECLSPEC void SDLCALL TTF_ClearFallbackFonts(TTF_Font *font);
+
 /**
  * Set a font's size dynamically.
  *
diff --git a/src/SDL_ttf.c b/src/SDL_ttf.c
index 73a8c31c..4bff177a 100644
--- a/src/SDL_ttf.c
+++ b/src/SDL_ttf.c
@@ -225,6 +225,12 @@ typedef struct PosBuf {
     int offset;
 } PosBuf_t;
 
+// A structure maintaining a list of fonts
+typedef struct TTF_FontList {
+    TTF_Font *font;
+    struct TTF_FontList *next;
+} TTF_FontList;
+
 // The structure used to hold internal font information
 struct TTF_Font {
     // Freetype2 maintains all sorts of useful info itself
@@ -299,6 +305,10 @@ struct TTF_Font {
 
     // Extra layout setting for wrapped text
     TTF_HorizontalAlignment horizontal_align;
+
+    // Fallback fonts
+    TTF_FontList *fallbacks;
+    TTF_FontList *fallback_for;
 };
 
 typedef struct
@@ -2113,16 +2123,25 @@ static bool RemoveFontTextReference(TTF_Font *font, TTF_Text *text)
     return SDL_RemoveFromHashTable(font->text, text);
 }
 
-static void UpdateFontText(TTF_Font *font)
+static void UpdateFontText(TTF_Font *font, TTF_Font *initial_font)
 {
-    if (!font->text) {
+    if (!initial_font) {
+        initial_font = font;
+    } else if (font == initial_font) {
+        // font fallback loop
         return;
     }
 
-    TTF_Text *text = NULL;
-    void *iter = NULL;
-    while (SDL_IterateHashTable(font->text, (const void **)&text, NULL, &iter)) {
-        text->internal->needs_layout_update = true;
+    if (font->text) {
+        TTF_Text *text = NULL;
+        void *iter = NULL;
+        while (SDL_IterateHashTable(font->text, (const void **)&text, NULL, &iter)) {
+            text->internal->needs_layout_update = true;
+        }
+    }
+
+    for (TTF_FontList *list = font->fallback_for; list; list = list->next) {
+        UpdateFontText(list->font, initial_font);
     }
 }
 
@@ -2143,6 +2162,91 @@ Uint32 TTF_GetFontGeneration(TTF_Font *font)
     return font->generation;
 }
 
+bool TTF_AddFallbackFont(TTF_Font *font, TTF_Font *fallback)
+{
+    TTF_CHECK_FONT(font, false);
+    TTF_CHECK_POINTER("fallback", fallback, false);
+
+    TTF_FontList *fallback_entry = (TTF_FontList *)SDL_calloc(1, sizeof(*fallback_entry));
+    TTF_FontList *fallback_for_entry = (TTF_FontList *)SDL_calloc(1, sizeof(*fallback_entry));
+    if (!fallback_entry || !fallback_for_entry) {
+        SDL_free(fallback_entry);
+        SDL_free(fallback_for_entry);
+        return false;
+    }
+
+    TTF_FontList *prev = NULL;
+    for (TTF_FontList *list = font->fallbacks; list; prev = list, list = list->next) {
+        continue;
+    }
+    fallback_entry->font = fallback;
+    if (prev) {
+        prev->next = fallback_entry;
+    } else {
+        font->fallbacks = fallback_entry;
+    }
+
+    prev = NULL;
+    for (TTF_FontList *list = fallback->fallback_for; list; prev = list, list = list->next) {
+        continue;
+    }
+    fallback_for_entry->font = font;
+    if (prev) {
+        prev->next = fallback_for_entry;
+    } else {
+        fallback->fallback_for = fallback_for_entry;
+    }
+
+    UpdateFontText(font, NULL);
+    return true;
+}
+
+void TTF_RemoveFallbackFont(TTF_Font *font, TTF_Font *fallback)
+{
+    if (!font || !fallback) {
+        return;
+    }
+
+    TTF_FontList *prev = NULL;
+    for (TTF_FontList *list = font->fallbacks; list; prev = list, list = list->next) {
+        if (fallback == list->font) {
+            if (prev) {
+                prev->next = list->next;
+            } else {
+                font->fallbacks = list->next;
+            }
+            SDL_free(list);
+            break;
+        }
+    }
+
+    prev = NULL;
+    for (TTF_FontList *list = fallback->fallback_for; list; prev = list, list = list->next) {
+        if (font == list->font) {
+            if (prev) {
+                prev->next = list->next;
+            } else {
+                fallback->fallback_for = list->next;
+            }
+            SDL_free(list);
+            break;
+        }
+    }
+
+    UpdateFontText(font, NULL);
+}
+
+void TTF_ClearFallbackFonts(TTF_Font *font)
+{
+    if (!font) {
+        return;
+    }
+
+    while (font->fallbacks) {
+        TTF_RemoveFallbackFont(font, font->fallbacks->font);
+    }
+}
+
 // Update font parameter depending on a style change
 static void TTF_InitFontMetrics(TTF_Font *font)
 {
@@ -2853,20 +2957,52 @@ static FT_UInt get_char_index(TTF_Font *font, Uint32 ch)
     return FT_Get_Char_Index(font->face, ch);
 }
 
+static FT_UInt get_char_index_fallback(TTF_Font *font, Uint32 ch, TTF_Font *initial_font, TTF_Font **glyph_font)
+{
+    if (!initial_font) {
+        initial_font = font;
+    } else if (font == initial_font) {
+        // font fallback loop
+        return 0;
+    }
+
+    FT_UInt idx = get_char_index(font, ch);
+    if (idx > 0) {
+        if (glyph_font) {
+            *glyph_font = font;
+        }
+    } else {
+        for (TTF_FontList *list = font->fallbacks; list; list = list->next) {
+            idx = get_char_index_fallback(list->font, ch, initial_font, glyph_font);
+            if (idx > 0) {
+                break;
+            }
+        }
+    }
+    return idx;
+}
+
+
 
-static bool Find_GlyphMetrics(TTF_Font *font, Uint32 ch, c_glyph **out_glyph)
+static bool Find_GlyphMetrics(TTF_Font *font, Uint32 ch, c_glyph **out_glyph, bool allow_fallback)
 {
     TTF_CHECK_FONT(font, false);
 
-    FT_UInt idx = get_char_index(font, ch);
-    return Find_GlyphByIndex(font, idx, 0, 0, 0, 0, 0, 0, out_glyph, NULL);
+    TTF_Font *glyph_font = font;
+    FT_UInt idx;
+    if (allow_fallback) {
+        idx = get_char_index_fallback(font, ch, NULL, &glyph_font);
+    } else {
+        idx = get_char_index(font, ch);
+    }
+    return Find_GlyphByIndex(glyph_font, idx, 0, 0, 0, 0, 0, 0, out_glyph, NULL);
 }
 
 bool TTF_FontHasGlyph(TTF_Font *font, Uint32 ch)
 {
     TTF_CHECK_FONT(font, false);
 
-    return (get_char_index(font, ch) > 0);
+    return (get_char_index_fallback(font, ch, NULL, NULL) > 0);
 }
 
 SDL_Surface *TTF_GetGlyphImage(TTF_Font *font, Uint32 ch)
@@ -2875,13 +3011,14 @@ SDL_Surface *TTF_GetGlyphImage(TTF_Font *font, Uint32 ch)
 
     TTF_CHECK_FONT(font, NULL);
 
-    idx = get_char_index(font, ch);
+    TTF_Font *glyph_font = NULL;
+    idx = get_char_index_fallback(font, ch, NULL, &glyph_font);
     if (idx == 0) {
         SDL_SetError("Codepoint not in font");
         return NULL;
     }
 
-    return TTF_GetGlyphImageForIndex(font, idx);
+    return TTF_GetGlyphImageForIndex(glyph_font, idx);
 }
 
 SDL_Surface *TTF_GetGlyphImageForIndex(TTF_Font *font, Uint32 glyph_index)
@@ -2939,7 +3076,7 @@ bool TTF_GetGlyphMetrics(TTF_Font *font, Uint32 ch, int *minx, int *maxx, int *m
 
     TTF_CHECK_FONT(font, false);
 
-    if (!Find_GlyphMetrics(font, ch, &glyph)) {
+    if (!Find_GlyphMetrics(font, ch, &glyph, true)) {
         return false;
     }
 
@@ -2983,21 +3120,16 @@ bool TTF_GetGlyphKerning(TTF_Font *font, Uint32 previous_ch, Uint32 ch, int *ker
         return true;
     }
 
-    if (!Find_GlyphMetrics(font, ch, &glyph)) {
-        return false;
-    }
-
-    if (!Find_GlyphMetrics(font, previous_ch, &prev_glyph)) {
-        return false;
-    }
-
-    error = FT_Get_Kerning(font->face, prev_glyph->index, glyph->index, FT_KERNING_DEFAULT, &delta);
-    if (error) {
-        return TTF_SetFTError("Couldn't get glyph kerning", error);
-    }
+    if (Find_GlyphMetrics(font, ch, &glyph, false) &&
+        Find_GlyphMetrics(font, previous_ch, &prev_glyph, false)) {
+        error = FT_Get_Kerning(font->face, prev_glyph->index, glyph->index, FT_KERNING_DEFAULT, &delta);
+        if (error) {
+            return TTF_SetFTError("Couldn't get glyph kerning", error);
+        }
 
-    if (kerning) {
-        *kerning = (int)(delta.x >> 6);
+        if (kerning) {
+            *kerning = (int)(delta.x >> 6);
+        }
     }
     return true;
 }
@@ -3021,6 +3153,7 @@ static bool TTF_Size_Internal(TTF_Font *font, const char *text, size_t length, i
     int advance_if_bold = 0;
 #else
     int skip_first = 1;
+    TTF_Font *prev_font = NULL;
     FT_UInt prev_index = 0;
     FT_Pos  prev_delta = 0;
 #endif
@@ -3113,7 +3246,7 @@ static bool TTF_Size_Internal(TTF_Font *font, const char *text, size_t length, i
         offset = (int)(text - start);
         Uint32 c = SDL_StepUTF8(&text, &length);
         TTF_Font *glyph_font = font;
-        FT_UInt idx = get_char_index(glyph_font, c);
+        FT_UInt idx = get_char_index_fallback(font, c, NULL, &glyph_font);
 
         if (c == UNICODE_BOM_NATIVE || c == UNICODE_BOM_SWAPPED) {
             continue;
@@ -3151,11 +3284,12 @@ static bool TTF_Size_Internal(TTF_Font *font, const char *text, size_t length, i
         x += prev_advance;
         prev_advance = glyph->advance;
         if (font->use_kerning) {
-            if (prev_index && glyph->index) {
+            if (prev_font == glyph_font && prev_index && glyph->index) {
                 FT_Vector delta;
                 FT_Get_Kerning(glyph_font->face, prev_index, glyph->index, FT_KERNING_UNFITTED, &delta);
                 x += delta.x;
             }
+            prev_font = glyph_font;
             prev_index = glyph->index;
         }
         // FT SUBPIXEL : LCD_MODE_LIGHT_SUBPIXEL
@@ -4957,7 +5091,7 @@ bool TTF_SetFontSizeDPI(TTF_Font *font, float ptsize, int hdpi, int vdpi)
     font->vdpi = vdpi;
 
     Flush_Cache(font);
-    UpdateFontText(font);
+    UpdateFontText(font, NULL);
 
 #if TTF_USE_HARFBUZZ
     // Call when size or variations settings on underlying FT_Face change.
@@ -5024,7 +5158,7 @@ void TTF_SetFontStyle(TTF_Font *font, TTF_FontStyleFlags style)
     if ((font->style | TTF_STYLE_NO_GLYPH_CHANGE) != (prev_style | TTF_STYLE_NO_GLYPH_CHANGE)) {
         Flush_Cache(font);
     }
-    UpdateFontText(font);
+    UpdateFontText(font, NULL);
 }
 
 TTF_FontStyleFlags TTF_GetFontStyle(const TTF_Font *font)
@@ -5086,7 +5220,7 @@ bool TTF_SetFontOutline(TTF_Font *font, int outline)
 
     TTF_InitFontMetrics(font);
     Flush_Cache(font);
-    UpdateFontText(font);
+    UpdateFontText(font, NULL);
 
     return true;
 }
@@ -5127,7 +5261,7 @@ void TTF_SetFontHinting(TTF_Font *font, TTF_HintingFlags hinting)
 #endif
 
     Flush_Cache(font);
-    UpdateFontText(font);
+    UpdateFontText(font, NULL);
 }
 
 TTF_HintingFlags TTF_GetFontHinting(const TTF_Font *font)
@@ -5154,7 +5288,7 @@ bool TTF_SetFontSDF(TTF_Font *font, bool enabled)
 #if TTF_USE_SDF
     font->render_sdf = enabled;
     Flush_Cache(font);
-    UpdateFontText(font);
+    UpdateFontText(font, NULL);
     return true;
 #else
     (void)enabled;
@@ -5187,7 +5321,7 @@ void TTF_SetFontWrapAlignment(TTF_Font *font, TTF_HorizontalAlignment align)
         // Ignore invalid values
         break;
     }
-    UpdateFontText(font);
+    UpdateFontText(font, NULL);
 }
 
 TTF_HorizontalAlignment TTF_GetFontWrapAlignment(const TTF_Font *font)
@@ -5227,7 +5361,7 @@ void TTF_SetFontLineSkip(TTF_Font *font, int lineskip)
     }
 
     font->lineskip = lineskip;
-    UpdateFontText(font);
+    UpdateFontText(font, NULL);
 }
 
 int TTF_GetFontLineSkip(const TTF_Font *font)
@@ -5251,7 +5385,7 @@ void TTF_SetFontKerning(TTF_Font *font, bool enabled)
 #else
     font->use_kerning   = enabled && FT_HAS_KERNING(font->face);
 #endif
-    UpdateFontText(font);
+    UpdateFontText(font, NULL);
 }
 
 bool TTF_GetFontKerning(const TTF_Font *font)
@@ -5319,7 +5453,7 @@ bool TTF_SetFontDirection(TTF_Font *font, TTF_Direction direction)
     }
 
     font->hb_direction = hb_direction;
-    UpdateFontText(font);
+    UpdateFontText(font, NULL);
     return true;
 #else
     (void) direction;
@@ -5373,7 +5507,7 @@ bool TTF_SetFontScript(TTF_Font *font, const char *script)
     }
 
     font->hb_script = hb_script;
-    UpdateFontText(font);
+    UpdateFontText(font, NULL);
     return true;
 #else
     (void) script;
@@ -5441,7 +5575,7 @@ bool TTF_SetFontLanguage(TTF_Font *font, const char *language_bcp47)
     }
 
     font->hb_language = hb_language;
-    UpdateFontText(font);
+    UpdateFontText(font, NULL);
     return true;
 #else
     (void) language_bcp47;
@@ -5472,6 +5606,11 @@ void TTF_CloseFont(TTF_Font *font)
     }
     Flush_Cache(font);
 
+    TTF_ClearFallbackFonts(font);
+    while (font->fallback_for) {
+        TTF_RemoveFallbackFont(font->fallback_for->font, font);
+    }
+
 #if TTF_USE_HARFBUZZ
     hb_font_destroy(font->hb_font);
 #endif
diff --git a/src/SDL_ttf.sym b/src/SDL_ttf.sym
index bfed9146..ef188a7b 100644
--- a/src/SDL_ttf.sym
+++ b/src/SDL_ttf.sym
@@ -1,6 +1,8 @@
 SDL3_ttf_0.0.0 {
   global:
+    TTF_AddFallbackFont;
     TTF_AppendTextString;
+    TTF_ClearFallbackFonts;
     TTF_CloseFont;
     TTF_CreateGPUTextEngine;
     TTF_CreateRendererTextEngine;
@@ -64,6 +66,7 @@ SDL3_ttf_0.0.0 {
     TTF_OpenFontIO;
     TTF_OpenFontWithProperties;
     TTF_Quit;
+    TTF_RemoveFallbackFont;
     TTF_RenderGlyph_Blended;
     TTF_RenderGlyph_LCD;
     TTF_RenderGlyph_Shaded;