SDL_ttf: Fixed glyph hash collisions with fallback fonts

From e4866be91ee81037701fc82bc8262f287bfa62b3 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Tue, 28 Jan 2025 06:13:28 -0800
Subject: [PATCH] Fixed glyph hash collisions with fallback fonts

---
 CMakeLists.txt                          |  1 +
 VisualC/SDL_ttf.vcxproj                 |  3 +-
 VisualC/SDL_ttf.vcxproj.filters         |  5 +-
 Xcode/SDL_ttf.xcodeproj/project.pbxproj |  8 +++
 src/SDL_gpu_textengine.c                | 35 ++++++-----
 src/SDL_hashtable_ttf.c                 | 82 +++++++++++++++++++++++++
 src/SDL_hashtable_ttf.h                 | 28 +++++++++
 src/SDL_renderer_textengine.c           | 35 ++++++-----
 src/SDL_surface_textengine.c            | 13 ++--
 9 files changed, 167 insertions(+), 43 deletions(-)
 create mode 100644 src/SDL_hashtable_ttf.c
 create mode 100644 src/SDL_hashtable_ttf.h

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 3bff411b..c0768b8a 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -125,6 +125,7 @@ endif()
 
 add_library(${sdl3_ttf_target_name}
     src/SDL_hashtable.c
+    src/SDL_hashtable_ttf.c
     src/SDL_gpu_textengine.c
     src/SDL_renderer_textengine.c
     src/SDL_surface_textengine.c
diff --git a/VisualC/SDL_ttf.vcxproj b/VisualC/SDL_ttf.vcxproj
index a33a84cb..ee86262a 100644
--- a/VisualC/SDL_ttf.vcxproj
+++ b/VisualC/SDL_ttf.vcxproj
@@ -380,6 +380,7 @@
     <ClCompile Include="..\external\harfbuzz\src\hb-wasm-shape.cc" />
     <ClCompile Include="..\src\SDL_gpu_textengine.c" />
     <ClCompile Include="..\src\SDL_hashtable.c" />
+    <ClCompile Include="..\src\SDL_hashtable_ttf.c" />
     <ClCompile Include="..\src\SDL_renderer_textengine.c" />
     <ClCompile Include="..\src\SDL_surface_textengine.c" />
     <ClCompile Include="..\src\SDL_ttf.c" />
@@ -393,4 +394,4 @@
   <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
   <ImportGroup Label="ExtensionTargets">
   </ImportGroup>
-</Project>
\ No newline at end of file
+</Project>
diff --git a/VisualC/SDL_ttf.vcxproj.filters b/VisualC/SDL_ttf.vcxproj.filters
index fd3efc5e..0b11c29b 100644
--- a/VisualC/SDL_ttf.vcxproj.filters
+++ b/VisualC/SDL_ttf.vcxproj.filters
@@ -24,6 +24,9 @@
     <ClCompile Include="..\src\SDL_hashtable.c">
       <Filter>Sources</Filter>
     </ClCompile>
+    <ClCompile Include="..\src\SDL_hashtable_ttf.c">
+      <Filter>Sources</Filter>
+    </ClCompile>
     <ClCompile Include="..\src\SDL_renderer_textengine.c">
       <Filter>Sources</Filter>
     </ClCompile>
@@ -415,4 +418,4 @@
       <Filter>x64</Filter>
     </CustomBuild>
   </ItemGroup>
-</Project>
\ No newline at end of file
+</Project>
diff --git a/Xcode/SDL_ttf.xcodeproj/project.pbxproj b/Xcode/SDL_ttf.xcodeproj/project.pbxproj
index 4685184f..08be75a7 100644
--- a/Xcode/SDL_ttf.xcodeproj/project.pbxproj
+++ b/Xcode/SDL_ttf.xcodeproj/project.pbxproj
@@ -65,6 +65,8 @@
 		F341242A2D42C44700D6C2B7 /* INSTALL.md in Resources */ = {isa = PBXBuildFile; fileRef = F34124292D42C44700D6C2B7 /* INSTALL.md */; };
 		F341242D2D42C47A00D6C2B7 /* LICENSE.txt in Resources */ = {isa = PBXBuildFile; fileRef = F341242B2D42C47A00D6C2B7 /* LICENSE.txt */; };
 		F341242E2D42C47A00D6C2B7 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = F341242C2D42C47A00D6C2B7 /* README.md */; };
+		F34125C72D491AA800D6C2B7 /* SDL_hashtable_ttf.h in Headers */ = {isa = PBXBuildFile; fileRef = F34125C52D491AA800D6C2B7 /* SDL_hashtable_ttf.h */; };
+		F34125C82D491AA800D6C2B7 /* SDL_hashtable_ttf.c in Sources */ = {isa = PBXBuildFile; fileRef = F34125C62D491AA800D6C2B7 /* SDL_hashtable_ttf.c */; };
 		F34400402D4033CE003F26D7 /* SDL3.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F344003F2D4033CE003F26D7 /* SDL3.framework */; };
 		F344FFBF2D3EB53C003F26D7 /* SDL_gpu_textengine.c in Sources */ = {isa = PBXBuildFile; fileRef = F344FFBE2D3EB53C003F26D7 /* SDL_gpu_textengine.c */; };
 		F3696FE4278F7107003A7F94 /* sdf.c in Sources */ = {isa = PBXBuildFile; fileRef = F3696FE3278F7107003A7F94 /* sdf.c */; };
@@ -191,6 +193,8 @@
 		F341242B2D42C47A00D6C2B7 /* LICENSE.txt */ = {isa = PBXFileReference; lastKnownFileType = text; name = LICENSE.txt; path = ../LICENSE.txt; sourceTree = SOURCE_ROOT; };
 		F341242C2D42C47A00D6C2B7 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = SOURCE_ROOT; };
 		F341242F2D42C49B00D6C2B7 /* INSTALL.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = INSTALL.md; sourceTree = "<group>"; };
+		F34125C52D491AA800D6C2B7 /* SDL_hashtable_ttf.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SDL_hashtable_ttf.h; sourceTree = "<group>"; };
+		F34125C62D491AA800D6C2B7 /* SDL_hashtable_ttf.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = SDL_hashtable_ttf.c; sourceTree = "<group>"; };
 		F344003F2D4033CE003F26D7 /* SDL3.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = SDL3.framework; sourceTree = "<group>"; };
 		F344FFBE2D3EB53C003F26D7 /* SDL_gpu_textengine.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = SDL_gpu_textengine.c; sourceTree = "<group>"; };
 		F3696FE3278F7107003A7F94 /* sdf.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = sdf.c; path = ../external/freetype/src/sdf/sdf.c; sourceTree = "<group>"; };
@@ -335,6 +339,8 @@
 				F344FFBE2D3EB53C003F26D7 /* SDL_gpu_textengine.c */,
 				F3F7BDF12CB6FD6700C984AF /* SDL_hashtable.h */,
 				F3F7BDF22CB6FD6700C984AF /* SDL_hashtable.c */,
+				F34125C52D491AA800D6C2B7 /* SDL_hashtable_ttf.h */,
+				F34125C62D491AA800D6C2B7 /* SDL_hashtable_ttf.c */,
 				F3F7BDF32CB6FD6700C984AF /* SDL_renderer_textengine.c */,
 				F3F7BDF42CB6FD6700C984AF /* SDL_surface_textengine.c */,
 				F567D67A01CD962A01F3E8B9 /* SDL_ttf.c */,
@@ -500,6 +506,7 @@
 			buildActionMask = 2147483647;
 			files = (
 				F3F7BDF72CB6FD6700C984AF /* SDL_hashtable.h in Headers */,
+				F34125C72D491AA800D6C2B7 /* SDL_hashtable_ttf.h in Headers */,
 				F3F7BDF82CB6FD6700C984AF /* stb_rect_pack.h in Headers */,
 				BE48FD5F07AFA17000BB41DA /* SDL_ttf.h in Headers */,
 				F33F083D2CC41C810062C26D /* SDL_textengine.h in Headers */,
@@ -648,6 +655,7 @@
 				F384BDBF261EC7650028A248 /* hb-buffer.cc in Sources */,
 				F384BD83261EC7650028A248 /* hb-shaper.cc in Sources */,
 				F384BCD4261EC2BE0028A248 /* type42.c in Sources */,
+				F34125C82D491AA800D6C2B7 /* SDL_hashtable_ttf.c in Sources */,
 				F384BDEF261EC7650028A248 /* hb-face.cc in Sources */,
 				F384BCDF261EC2CF0028A248 /* winfnt.c in Sources */,
 				F384BC2F261EC1710028A248 /* cff.c in Sources */,
diff --git a/src/SDL_gpu_textengine.c b/src/SDL_gpu_textengine.c
index 35c05742..5479702e 100644
--- a/src/SDL_gpu_textengine.c
+++ b/src/SDL_gpu_textengine.c
@@ -23,6 +23,7 @@
 #include <SDL3_ttf/SDL_ttf.h>
 
 #include "SDL_hashtable.h"
+#include "SDL_hashtable_ttf.h"
 
 #define ATLAS_TEXTURE_SIZE 1024
 
@@ -387,9 +388,9 @@ static bool UpdateGlyph(SDL_GPUDevice *device, AtlasGlyph *glyph, SDL_Surface *s
     return true;
 }
 
-static bool AddGlyphToFont(TTF_GPUTextEngineFontData *fontdata, Uint32 glyph_index, AtlasGlyph *glyph)
+static bool AddGlyphToFont(TTF_GPUTextEngineFontData *fontdata, TTF_Font *glyph_font, Uint32 glyph_index, AtlasGlyph *glyph)
 {
-    if (!SDL_InsertIntoHashTable(fontdata->glyphs, (const void *)(uintptr_t)glyph_index, glyph)) {
+    if (!SDL_InsertIntoGlyphHashTable(fontdata->glyphs, glyph_font, glyph_index, glyph)) {
         return false;
     }
     return true;
@@ -411,12 +412,13 @@ static bool ResolveMissingGlyphs(TTF_GPUTextEngineData *enginedata, AtlasTexture
                 return false;
             }
 
-            if (!AddGlyphToFont(fontdata, ops[missing[i].id].copy.glyph_index, glyph)) {
+            TTF_DrawOperation *op = &ops[missing[i].id];
+            if (!AddGlyphToFont(fontdata, op->copy.glyph_font, op->copy.glyph_index, glyph)) {
                 ReleaseGlyph(glyph);
                 return false;
             }
 
-            ops[missing[i].id].copy.reserved = glyph;
+            op->copy.reserved = glyph;
 
             // Remove this from the missing entries
             --num_missing;
@@ -447,12 +449,13 @@ static bool ResolveMissingGlyphs(TTF_GPUTextEngineData *enginedata, AtlasTexture
             return false;
         }
 
-        if (!AddGlyphToFont(fontdata, ops[missing[i].id].copy.glyph_index, glyph)) {
+        TTF_DrawOperation *op = &ops[missing[i].id];
+        if (!AddGlyphToFont(fontdata, op->copy.glyph_font, op->copy.glyph_index, glyph)) {
             ReleaseGlyph(glyph);
             return false;
         }
 
-        ops[missing[i].id].copy.reserved = glyph;
+        op->copy.reserved = glyph;
     }
 
     if (all_packed) {
@@ -496,7 +499,7 @@ static bool CreateMissingGlyphs(TTF_GPUTextEngineData *enginedata, TTF_GPUTextEn
         goto done;
     }
 
-    checked = SDL_CreateHashTable(NULL, 4, SDL_HashID, SDL_KeyMatchID, NULL, false, false);
+    checked = SDL_CreateGlyphHashTable(NULL);
     if (!checked) {
         goto done;
     }
@@ -507,10 +510,10 @@ static bool CreateMissingGlyphs(TTF_GPUTextEngineData *enginedata, TTF_GPUTextEn
         if (op->cmd == TTF_DRAW_COMMAND_COPY && !op->copy.reserved) {
             TTF_Font *glyph_font = op->copy.glyph_font;
             Uint32 glyph_index = op->copy.glyph_index;
-            if (SDL_FindInHashTable(checked, (const void *)(uintptr_t)glyph_index, NULL)) {
+            if (SDL_FindInGlyphHashTable(checked, glyph_font, glyph_index, NULL)) {
                 continue;
             }
-            if (!SDL_InsertIntoHashTable(checked, (const void *)(uintptr_t)glyph_index, NULL)) {
+            if (!SDL_InsertIntoGlyphHashTable(checked, glyph_font, glyph_index, NULL)) {
                 goto done;
             }
 
@@ -553,7 +556,7 @@ static bool CreateMissingGlyphs(TTF_GPUTextEngineData *enginedata, TTF_GPUTextEn
     for (int i = 0; i < num_ops; ++i) {
         TTF_DrawOperation *op = &ops[i];
         if (op->cmd == TTF_DRAW_COMMAND_COPY && !op->copy.reserved) {
-            if (!SDL_FindInHashTable(fontdata->glyphs, (const void *)(uintptr_t)op->copy.glyph_index, (const void **)&op->copy.reserved)) {
+            if (!SDL_FindInGlyphHashTable(fontdata->glyphs, op->copy.glyph_font, op->copy.glyph_index, (const void **)&op->copy.reserved)) {
                 // Something is very wrong...
                 goto done;
             }
@@ -563,7 +566,7 @@ static bool CreateMissingGlyphs(TTF_GPUTextEngineData *enginedata, TTF_GPUTextEn
     result = true;
 
 done:
-    SDL_DestroyHashTable(checked);
+    SDL_DestroyGlyphHashTable(checked);
     if (surfaces) {
         for (int i = 0; i < num_ops; ++i) {
             SDL_DestroySurface(surfaces[i]);
@@ -747,7 +750,7 @@ static TTF_GPUTextEngineTextData *CreateTextData(TTF_GPUTextEngineData *engineda
 
         ++num_glyphs;
 
-        if (!SDL_FindInHashTable(fontdata->glyphs, (const void *)(uintptr_t)op->copy.glyph_index, (const void **)&op->copy.reserved)) {
+        if (!SDL_FindInGlyphHashTable(fontdata->glyphs, op->copy.glyph_font, op->copy.glyph_index, (const void **)&op->copy.reserved)) {
             ++num_missing;
         }
     }
@@ -791,17 +794,15 @@ static void DestroyFontData(TTF_GPUTextEngineFontData *data)
 {
     if (data) {
         if (data->glyphs) {
-            SDL_DestroyHashTable(data->glyphs);
+            SDL_DestroyGlyphHashTable(data->glyphs);
         }
         SDL_free(data);
     }
 }
 
-static void NukeGlyph(const void *key, const void *value, void *unused)
+static void NukeGlyph(const void *value)
 {
     AtlasGlyph *glyph = (AtlasGlyph *)value;
-    (void)key;
-    (void)unused;
     ReleaseGlyph(glyph);
 }
 
@@ -813,7 +814,7 @@ static TTF_GPUTextEngineFontData *CreateFontData(TTF_GPUTextEngineData *engineda
     }
     data->font = font;
     data->generation = font_generation;
-    data->glyphs = SDL_CreateHashTable(NULL, 4, SDL_HashID, SDL_KeyMatchID, NukeGlyph, false, false);
+    data->glyphs = SDL_CreateGlyphHashTable(NukeGlyph);
     if (!data->glyphs) {
         DestroyFontData(data);
         return NULL;
diff --git a/src/SDL_hashtable_ttf.c b/src/SDL_hashtable_ttf.c
new file mode 100644
index 00000000..e14c2560
--- /dev/null
+++ b/src/SDL_hashtable_ttf.c
@@ -0,0 +1,82 @@
+/*
+  SDL_ttf:  A companion library to SDL for working with TrueType (tm) fonts
+  Copyright (C) 2001-2025 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.
+*/
+#include <SDL3_ttf/SDL_ttf.h>
+
+#include "SDL_hashtable.h"
+#include "SDL_hashtable_ttf.h"
+
+typedef struct GlyphHashtableKey {
+    TTF_Font *font;
+    Uint32 glyph_index;
+} GlyphHashtableKey;
+
+static Uint32 SDL_HashGlyphHashtableKey(const void *key, void *unused)
+{
+    (void)unused;
+    return SDL_murmur3_32(key, sizeof(GlyphHashtableKey), 0);
+}
+
+static bool SDL_KeyMatchGlyphHashtableKey(const void *a, const void *b, void *unused)
+{
+    (void)unused;
+    GlyphHashtableKey *A = (GlyphHashtableKey *)a;
+    GlyphHashtableKey *B = (GlyphHashtableKey *)b;
+    return (A->font == B->font && A->glyph_index == B->glyph_index);
+}
+
+static void SDL_NukeFreeGlyphHashtableKey(const void *key, const void *value, void *data)
+{
+    SDL_GlyphHashTable_NukeFn nukefn = (SDL_GlyphHashTable_NukeFn)data;
+
+    if (nukefn) {
+        nukefn(value);
+    }
+    SDL_free((void *)key);
+}
+
+SDL_HashTable *SDL_CreateGlyphHashTable(SDL_GlyphHashTable_NukeFn nukefn)
+{
+    const int num_buckets = 4; // FIXME: Is this a good value?
+    return SDL_CreateHashTable(nukefn, num_buckets, SDL_HashGlyphHashtableKey, SDL_KeyMatchGlyphHashtableKey, SDL_NukeFreeGlyphHashtableKey, false, false);
+}
+
+bool SDL_InsertIntoGlyphHashTable(SDL_HashTable *table, TTF_Font *font, Uint32 glyph_index, const void *value)
+{
+    GlyphHashtableKey *key = (GlyphHashtableKey *)SDL_calloc(1, sizeof(*key));
+    key->font = font;
+    key->glyph_index = glyph_index;
+    return SDL_InsertIntoHashTable(table, key, value);
+}
+
+bool SDL_FindInGlyphHashTable(SDL_HashTable *table, TTF_Font *font, Uint32 glyph_index, const void **value)
+{
+    GlyphHashtableKey key;
+    SDL_zero(key);
+    key.font = font;
+    key.glyph_index = glyph_index;
+    return SDL_FindInHashTable(table, &key, value);
+}
+
+void SDL_DestroyGlyphHashTable(SDL_HashTable *table)
+{
+    SDL_DestroyHashTable(table);
+}
+
diff --git a/src/SDL_hashtable_ttf.h b/src/SDL_hashtable_ttf.h
new file mode 100644
index 00000000..a19b82e5
--- /dev/null
+++ b/src/SDL_hashtable_ttf.h
@@ -0,0 +1,28 @@
+/*
+  SDL_ttf:  A companion library to SDL for working with TrueType (tm) fonts
+  Copyright (C) 2001-2025 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.
+*/
+
+typedef void (*SDL_GlyphHashTable_NukeFn)(const void *value);
+
+extern SDL_HashTable *SDL_CreateGlyphHashTable(SDL_GlyphHashTable_NukeFn nukefn);
+extern bool SDL_InsertIntoGlyphHashTable(SDL_HashTable *table, TTF_Font *font, Uint32 glyph_index, const void *value);
+extern  bool SDL_FindInGlyphHashTable(SDL_HashTable *table, TTF_Font *font, Uint32 glyph_index, const void **value);
+extern void SDL_DestroyGlyphHashTable(SDL_HashTable *table);
+
diff --git a/src/SDL_renderer_textengine.c b/src/SDL_renderer_textengine.c
index 3ff42e1f..d5521934 100644
--- a/src/SDL_renderer_textengine.c
+++ b/src/SDL_renderer_textengine.c
@@ -21,6 +21,7 @@
 #include <SDL3_ttf/SDL_textengine.h>
 
 #include "SDL_hashtable.h"
+#include "SDL_hashtable_ttf.h"
 
 #define ATLAS_TEXTURE_SIZE  1024
 
@@ -321,9 +322,9 @@ static bool UpdateGlyph(AtlasGlyph *glyph, SDL_Surface *surface)
     return true;
 }
 
-static bool AddGlyphToFont(TTF_RendererTextEngineFontData *fontdata, Uint32 glyph_index, AtlasGlyph *glyph)
+static bool AddGlyphToFont(TTF_RendererTextEngineFontData *fontdata, TTF_Font *glyph_font, Uint32 glyph_index, AtlasGlyph *glyph)
 {
-    if (!SDL_InsertIntoHashTable(fontdata->glyphs, (const void *)(uintptr_t)glyph_index, glyph)) {
+    if (!SDL_InsertIntoGlyphHashTable(fontdata->glyphs, glyph_font, glyph_index, glyph)) {
         return false;
     }
     return true;
@@ -345,12 +346,13 @@ static bool ResolveMissingGlyphs(TTF_RendererTextEngineData *enginedata, AtlasTe
                 return false;
             }
 
-            if (!AddGlyphToFont(fontdata, ops[missing[i].id].copy.glyph_index, glyph)) {
+            TTF_DrawOperation *op = &ops[missing[i].id];
+            if (!AddGlyphToFont(fontdata, op->copy.glyph_font, op->copy.glyph_index, glyph)) {
                 ReleaseGlyph(glyph);
                 return false;
             }
 
-            ops[missing[i].id].copy.reserved = glyph;
+            op->copy.reserved = glyph;
 
             // Remove this from the missing entries
             --num_missing;
@@ -381,12 +383,13 @@ static bool ResolveMissingGlyphs(TTF_RendererTextEngineData *enginedata, AtlasTe
             return false;
         }
 
-        if (!AddGlyphToFont(fontdata, ops[missing[i].id].copy.glyph_index, glyph)) {
+        TTF_DrawOperation *op = &ops[missing[i].id];
+        if (!AddGlyphToFont(fontdata, op->copy.glyph_font, op->copy.glyph_index, glyph)) {
             ReleaseGlyph(glyph);
             return false;
         }
 
-        ops[missing[i].id].copy.reserved = glyph;
+        op->copy.reserved = glyph;
     }
 
     if (all_packed) {
@@ -430,7 +433,7 @@ static bool CreateMissingGlyphs(TTF_RendererTextEngineData *enginedata, TTF_Rend
         goto done;
     }
 
-    checked = SDL_CreateHashTable(NULL, 4, SDL_HashID, SDL_KeyMatchID, NULL, false, false);
+    checked = SDL_CreateGlyphHashTable(NULL);
     if (!checked) {
         goto done;
     }
@@ -441,10 +444,10 @@ static bool CreateMissingGlyphs(TTF_RendererTextEngineData *enginedata, TTF_Rend
         if (op->cmd == TTF_DRAW_COMMAND_COPY && !op->copy.reserved) {
             TTF_Font *glyph_font = op->copy.glyph_font;
             Uint32 glyph_index = op->copy.glyph_index;
-            if (SDL_FindInHashTable(checked, (const void *)(uintptr_t)glyph_index, NULL)) {
+            if (SDL_FindInGlyphHashTable(checked, glyph_font, glyph_index, NULL)) {
                 continue;
             }
-            if (!SDL_InsertIntoHashTable(checked, (const void *)(uintptr_t)glyph_index, NULL)) {
+            if (!SDL_InsertIntoGlyphHashTable(checked, glyph_font, glyph_index, NULL)) {
                 goto done;
             }
 
@@ -486,7 +489,7 @@ static bool CreateMissingGlyphs(TTF_RendererTextEngineData *enginedata, TTF_Rend
     for (int i = 0; i < num_ops; ++i) {
         TTF_DrawOperation *op = &ops[i];
         if (op->cmd == TTF_DRAW_COMMAND_COPY && !op->copy.reserved) {
-            if (!SDL_FindInHashTable(fontdata->glyphs, (const void *)(uintptr_t)op->copy.glyph_index, (const void **)&op->copy.reserved)) {
+            if (!SDL_FindInGlyphHashTable(fontdata->glyphs, op->copy.glyph_font, op->copy.glyph_index, (const void **)&op->copy.reserved)) {
                 // Something is very wrong...
                 goto done;
             }
@@ -496,7 +499,7 @@ static bool CreateMissingGlyphs(TTF_RendererTextEngineData *enginedata, TTF_Rend
     result = true;
 
 done:
-    SDL_DestroyHashTable(checked);
+    SDL_DestroyGlyphHashTable(checked);
     if (surfaces) {
         for (int i = 0; i < num_ops; ++i) {
             SDL_DestroySurface(surfaces[i]);
@@ -659,7 +662,7 @@ static TTF_RendererTextEngineTextData *CreateTextData(TTF_RendererTextEngineData
 
         ++num_glyphs;
 
-        if (!SDL_FindInHashTable(fontdata->glyphs, (const void *)(uintptr_t)op->copy.glyph_index, (const void **)&op->copy.reserved)) {
+        if (!SDL_FindInGlyphHashTable(fontdata->glyphs, op->copy.glyph_font, op->copy.glyph_index, (const void **)&op->copy.reserved)) {
             ++num_missing;
         }
     }
@@ -703,17 +706,15 @@ static void DestroyFontData(TTF_RendererTextEngineFontData *data)
 {
     if (data) {
         if (data->glyphs) {
-            SDL_DestroyHashTable(data->glyphs);
+            SDL_DestroyGlyphHashTable(data->glyphs);
         }
         SDL_free(data);
     }
 }
 
-static void NukeGlyph(const void *key, const void *value, void *unused)
+static void NukeGlyph(const void *value)
 {
     AtlasGlyph *glyph = (AtlasGlyph *)value;
-    (void)key;
-    (void)unused;
     ReleaseGlyph(glyph);
 }
 
@@ -725,7 +726,7 @@ static TTF_RendererTextEngineFontData *CreateFontData(TTF_RendererTextEngineData
     }
     data->font = font;
     data->generation = font_generation;
-    data->glyphs = SDL_CreateHashTable(NULL, 4, SDL_HashID, SDL_KeyMatchID, NukeGlyph, false, false);
+    data->glyphs = SDL_CreateGlyphHashTable(NukeGlyph);
     if (!data->glyphs) {
         DestroyFontData(data);
         return NULL;
diff --git a/src/SDL_surface_textengine.c b/src/SDL_surface_textengine.c
index 5f1a3565..10e24978 100644
--- a/src/SDL_surface_textengine.c
+++ b/src/SDL_surface_textengine.c
@@ -21,6 +21,7 @@
 #include <SDL3_ttf/SDL_textengine.h>
 
 #include "SDL_hashtable.h"
+#include "SDL_hashtable_ttf.h"
 
 
 typedef struct TTF_SurfaceTextEngineGlyphData
@@ -84,7 +85,7 @@ static TTF_SurfaceTextEngineGlyphData *GetGlyphData(TTF_SurfaceTextEngineFontDat
 {
     TTF_SurfaceTextEngineGlyphData *data;
 
-    if (!SDL_FindInHashTable(fontdata->glyphs, (const void *)(uintptr_t)glyph_index, (const void **)&data)) {
+    if (!SDL_FindInGlyphHashTable(fontdata->glyphs, glyph_font, glyph_index, (const void **)&data)) {
         SDL_Surface *surface = TTF_GetGlyphImageForIndex(glyph_font, glyph_index);
         if (!surface) {
             return NULL;
@@ -95,7 +96,7 @@ static TTF_SurfaceTextEngineGlyphData *GetGlyphData(TTF_SurfaceTextEngineFontDat
             return NULL;
         }
 
-        if (!SDL_InsertIntoHashTable(fontdata->glyphs, (const void *)(uintptr_t)glyph_index, data)) {
+        if (!SDL_InsertIntoGlyphHashTable(fontdata->glyphs, glyph_font, glyph_index, data)) {
             DestroyGlyphData(data);
             return NULL;
         }
@@ -159,16 +160,14 @@ static void DestroyFontData(TTF_SurfaceTextEngineFontData *data)
     }
 
     if (data->glyphs) {
-        SDL_DestroyHashTable(data->glyphs);
+        SDL_DestroyGlyphHashTable(data->glyphs);
     }
     SDL_free(data);
 }
 
-static void NukeGlyphData(const void *key, const void *value, void *unused)
+static void NukeGlyphData(const void *value)
 {
     TTF_SurfaceTextEngineGlyphData *data = (TTF_SurfaceTextEngineGlyphData *)value;
-    (void)key;
-    (void)unused;
     DestroyGlyphData(data);
 }
 
@@ -181,7 +180,7 @@ static TTF_SurfaceTextEngineFontData *CreateFontData(TTF_SurfaceTextEngineData *
 
     data->font = font;
     data->generation = font_generation;
-    data->glyphs = SDL_CreateHashTable(NULL, 4, SDL_HashID, SDL_KeyMatchID, NukeGlyphData, false, false);
+    data->glyphs = SDL_CreateGlyphHashTable(NukeGlyphData);
     if (!data->glyphs) {
         DestroyFontData(data);
         return NULL;