SDL_ttf: Implemented font atlas support for the renderer text engine

From 9af2526267bfdc5a34c575f3a8f4b124539dbb16 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Mon, 30 Sep 2024 16:09:45 -0700
Subject: [PATCH] Implemented font atlas support for the renderer text engine

This turns most text drawing into a single draw call.
---
 src/SDL_renderer_textengine.c | 760 ++++++++++++++++++++++++++++------
 src/stb_rect_pack.h           | 623 ++++++++++++++++++++++++++++
 2 files changed, 1248 insertions(+), 135 deletions(-)
 create mode 100644 src/stb_rect_pack.h

diff --git a/src/SDL_renderer_textengine.c b/src/SDL_renderer_textengine.c
index c1ab1af0..ae2877c2 100644
--- a/src/SDL_renderer_textengine.c
+++ b/src/SDL_renderer_textengine.c
@@ -22,21 +22,52 @@
 
 #include "SDL_hashtable.h"
 
-/* Note: This is a naive implementation using one texture per glyph.
- *       We can easily turn this into a texture atlas implementation later.
- */
+#define ATLAS_TEXTURE_SIZE  256
 
-typedef struct TTF_RendererTextEngineGlyphData
+#define STB_RECT_PACK_IMPLEMENTATION
+#define STBRP_STATIC
+#define STBRP_SORT SDL_qsort
+#define STBRP_ASSERT SDL_assert
+#include "stb_rect_pack.h"
+
+typedef struct AtlasGlyph AtlasGlyph;
+typedef struct AtlasTexture AtlasTexture;
+typedef struct AtlasDrawSequence AtlasDrawSequence;
+
+struct AtlasGlyph
 {
     int refcount;
-    SDL_FColor color;
+    AtlasTexture *atlas;
+    SDL_Rect rect;
+    float texcoords[12];
+    AtlasGlyph *next;
+};
+
+struct AtlasTexture
+{
     SDL_Texture *texture;
-} TTF_RendererTextEngineGlyphData;
+    stbrp_context packer;
+    stbrp_node *packing_nodes;
+    int num_free_glyphs;
+    AtlasGlyph *free_glyphs;
+    AtlasTexture *next;
+};
+
+struct AtlasDrawSequence
+{
+    SDL_Texture *texture;
+    int num_rects;
+    SDL_Rect *rects;
+    float *texcoords;
+    float *positions;
+    AtlasDrawSequence *next;
+};
 
 typedef struct TTF_RendererTextEngineTextData
 {
-    TTF_DrawOperation *ops;
-    int num_ops;
+    int num_glyphs;
+    AtlasGlyph **glyphs;
+    AtlasDrawSequence *draw_sequence;
 } TTF_RendererTextEngineTextData;
 
 typedef struct TTF_RendererTextEngineFontData
@@ -50,66 +81,517 @@ typedef struct TTF_RendererTextEngineData
 {
     SDL_Renderer *renderer;
     SDL_HashTable *fonts;
+    AtlasTexture *atlas;
 } TTF_RendererTextEngineData;
 
 
-static void DestroyGlyphData(TTF_RendererTextEngineGlyphData *data)
+static int SDLCALL SortMissing(void *userdata, const void *a, const void *b)
 {
-    if (!data) {
+    const TTF_DrawOperation *ops = (const TTF_DrawOperation *)userdata;
+    const stbrp_rect *A = (const stbrp_rect *)a;
+    const stbrp_rect *B = (const stbrp_rect *)b;
+
+    // Sort missing first
+    if (!ops[A->id].copy.reserved) {
+        if (ops[B->id].copy.reserved) {
+            return -1;
+        }
+    }
+    if (!ops[B->id].copy.reserved) {
+        if (ops[A->id].copy.reserved) {
+            return 1;
+        }
+    }
+
+    // Sort largest first
+    if (A->w != B->w) {
+        if (A->w > B->w) {
+            return -1;
+        } else {
+            return 1;
+        }
+    }
+    if (A->h != B->h) {
+        if (A->h > B->h) {
+            return -1;
+        } else {
+            return 1;
+        }
+    }
+
+    // It doesn't matter, sort by ID
+    if (A->id < B->id) {
+        return -1;
+    } else {
+        return 1;
+    }
+}
+
+static int SDLCALL SortOperations(const void *a, const void *b)
+{
+    const TTF_DrawOperation *A = (const TTF_DrawOperation *)a;
+    const TTF_DrawOperation *B = (const TTF_DrawOperation *)b;
+
+    if (A->cmd == TTF_DRAW_COMMAND_COPY &&
+        B->cmd == TTF_DRAW_COMMAND_COPY) {
+        AtlasGlyph *glyphA = (AtlasGlyph *)A->copy.reserved;
+        AtlasGlyph *glyphB = (AtlasGlyph *)B->copy.reserved;
+        if (glyphA->atlas != glyphB->atlas) {
+            // It's not important how we sort this, just that it's consistent
+            return (glyphA->atlas < glyphB->atlas) ? -1 : 1;
+        }
+
+        // We could sort by texture coordinate or whatever, if we cared.
+        return 0;
+    }
+
+    if (A->cmd == TTF_DRAW_COMMAND_COPY) {
+        return -1;
+    }
+    if (B->cmd == TTF_DRAW_COMMAND_COPY) {
+        return 1;
+    }
+    return 0;
+}
+
+static void DestroyAtlas(AtlasTexture *atlas)
+{
+    if (!atlas) {
+        return;
+    }
+
+    SDL_DestroyTexture(atlas->texture);
+    SDL_free(atlas->packing_nodes);
+    SDL_free(atlas);
+}
+
+static AtlasTexture *CreateAtlas(SDL_Renderer *renderer)
+{
+    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);
+    if (!atlas->texture) {
+        DestroyAtlas(atlas);
+        return NULL;
+    }
+
+    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_setup_heuristic(&atlas->packer, STBRP_HEURISTIC_Skyline_default);
+
+    return atlas;
+}
+
+static void ReleaseGlyph(AtlasGlyph *glyph)
+{
+    if (!glyph) {
         return;
     }
 
-    --data->refcount;
-    if (data->refcount == 0) {
-        if (data->texture) {
-            SDL_DestroyTexture(data->texture);
+    --glyph->refcount;
+    if (glyph->refcount == 0) {
+        if (glyph->atlas) {
+            // Insert into free list sorted smallest first
+            AtlasGlyph *entry, *prev = NULL;
+            int size = (glyph->rect.w * glyph->rect.h);
+            for (entry = glyph->atlas->free_glyphs; entry; entry = entry->next) {
+                if (size <= (entry->rect.w * entry->rect.h)) {
+                    break;
+                }
+
+                prev = entry;
+            }
+
+            ++glyph->atlas->num_free_glyphs;
+            if (prev) {
+                prev->next = glyph;
+            } else {
+                glyph->atlas->free_glyphs = glyph;
+            }
+            glyph->next = entry;
+        } else {
+            SDL_free(glyph);
         }
-        SDL_free(data);
     }
 }
 
-static TTF_RendererTextEngineGlyphData *CreateGlyphData(SDL_Texture *texture)
+static AtlasGlyph *CreateGlyph(AtlasTexture *atlas, const stbrp_rect *area)
 {
-    TTF_RendererTextEngineGlyphData *data = (TTF_RendererTextEngineGlyphData *)SDL_malloc(sizeof(*data));
-    if (data) {
-        data->refcount = 1;
-        data->color.r = 1.0f;
-        data->color.g = 1.0f;
-        data->color.b = 1.0f;
-        data->color.a = 1.0f;
-        data->texture = texture;
+    AtlasGlyph *glyph = (AtlasGlyph *)SDL_calloc(1, sizeof(*glyph));
+    if (!glyph) {
+        return NULL;
     }
-    return data;
+
+    glyph->refcount = 1;
+    glyph->atlas = atlas;
+    glyph->rect.x = area->x;
+    glyph->rect.y = area->y;
+    glyph->rect.w = area->w;
+    glyph->rect.h = area->h;
+
+    const float minx = (float)area->x / ATLAS_TEXTURE_SIZE;
+    const float maxx = (float)(area->x + area->w) / ATLAS_TEXTURE_SIZE;
+    const float miny = (float)area->y / ATLAS_TEXTURE_SIZE;
+    const float maxy = (float)(area->y + area->h) / ATLAS_TEXTURE_SIZE;
+    glyph->texcoords[0] = minx;
+    glyph->texcoords[1] = miny;
+    glyph->texcoords[2] = maxx;
+    glyph->texcoords[3] = miny;
+    glyph->texcoords[4] = maxx;
+    glyph->texcoords[5] = maxy;
+    glyph->texcoords[6] = minx;
+    glyph->texcoords[7] = miny;
+    glyph->texcoords[8] = maxx;
+    glyph->texcoords[9] = maxy;
+    glyph->texcoords[10] = minx;
+    glyph->texcoords[11] = maxy;
+
+    return glyph;
 }
 
-static TTF_RendererTextEngineGlyphData *GetGlyphData(SDL_Renderer *renderer, TTF_RendererTextEngineFontData *fontdata, Uint32 idx)
+static AtlasGlyph *FindUnusedGlyph(AtlasTexture *atlas, int width, int height)
 {
-    TTF_RendererTextEngineGlyphData *data;
+    AtlasGlyph *glyph, *prev = NULL;
+
+    int size = (width * height);
+    for (glyph = atlas->free_glyphs; glyph; glyph = glyph->next) {
+        if (width == glyph->rect.w && height == glyph->rect.h) {
+            if (prev) {
+                prev->next = glyph->next;
+            } else {
+                atlas->free_glyphs = glyph->next;
+            }
+            --atlas->num_free_glyphs;
+            ++glyph->refcount;
+            return glyph;
+        }
 
-    if (!SDL_FindInHashTable(fontdata->glyphs, (const void *)(uintptr_t)idx, (const void **)&data)) {
-        SDL_Texture *texture;
-        SDL_Surface *surface = TTF_GetGlyphImageForIndex(fontdata->font, idx);
-        if (!surface) {
-            return NULL;
+        if (size < (glyph->rect.w * glyph->rect.h)) {
+            // We didn't find any entries our size, everything else is larger than we want
+            break;
         }
 
-        texture = SDL_CreateTextureFromSurface(renderer, surface);
-        SDL_DestroySurface(surface);
-        if (!texture) {
-            return NULL;
+        prev = glyph;
+    }
+
+    if (atlas->next) {
+        return FindUnusedGlyph(atlas->next, width, height);
+    }
+    return NULL;
+}
+
+static bool UpdateGlyph(AtlasGlyph *glyph, SDL_Surface *surface)
+{
+    SDL_Texture *texture = glyph->atlas->texture;
+    void *pixels;
+    int pitch;
+    if (!SDL_LockTexture(texture, &glyph->rect, &pixels, &pitch)) {
+        return false;
+    }
+
+    const Uint8 *src = (const Uint8 *)surface->pixels;
+    const int src_pitch = surface->pitch;
+    Uint8 *dst = (Uint8 *)pixels;
+    const int dst_pitch = pitch;
+    for (int i = 0; i < glyph->rect.h; ++i) {
+        SDL_memcpy(dst, src, glyph->rect.w * 4);
+        src += src_pitch;
+        dst += dst_pitch;
+    }
+    SDL_UnlockTexture(texture);
+    return true;
+}
+
+static bool AddGlyphToFont(TTF_RendererTextEngineFontData *fontdata, Uint32 glyph_index, AtlasGlyph *glyph)
+{
+    if (!SDL_InsertIntoHashTable(fontdata->glyphs, (const void *)(uintptr_t)glyph_index, glyph)) {
+        return false;
+    }
+    return true;
+}
+
+static bool ResolveMissingGlyphs(TTF_RendererTextEngineData *enginedata, AtlasTexture *atlas, TTF_RendererTextEngineFontData *fontdata, SDL_Surface **surfaces, TTF_DrawOperation *ops, int num_ops, stbrp_rect *missing, int num_missing)
+{
+    // See if we can reuse any existing entries
+    if (atlas->free_glyphs) {
+        // Search from the smallest to the largest to minimize time spent searching the free list and shortening the missing entries
+        for (int i = num_missing; i--; ) {
+            AtlasGlyph *glyph = FindUnusedGlyph(atlas, missing[i].w, missing[i].h);
+            if (!glyph) {
+                continue;
+            }
+
+            if (!UpdateGlyph(glyph, surfaces[missing[i].id])) {
+                ReleaseGlyph(glyph);
+                return false;
+            }
+
+            if (!AddGlyphToFont(fontdata, ops[missing[i].id].copy.glyph_index, glyph)) {
+                ReleaseGlyph(glyph);
+                return false;
+            }
+
+            ops[missing[i].id].copy.reserved = glyph;
+
+            // Remove this from the missing entries
+            --num_missing;
+            if (i < num_missing) {
+                SDL_memcpy(&missing[i], &missing[i+1], (num_missing - i) * sizeof(missing[i]));
+            }
+        }
+        if (num_missing == 0) {
+            return true;
+        }
+    }
+
+    // Try to pack all the missing glyphs into the current atlas
+    bool all_packed = (stbrp_pack_rects(&atlas->packer, missing, num_missing) == 1);
+
+    for (int i = 0; i < num_missing; ++i) {
+        if (!missing[i].was_packed) {
+            continue;
+        }
+
+        AtlasGlyph *glyph = CreateGlyph(atlas, &missing[i]);
+        if (!glyph) {
+            return false;
+        }
+
+        if (!UpdateGlyph(glyph, surfaces[missing[i].id])) {
+            ReleaseGlyph(glyph);
+            return false;
+        }
+
+        if (!AddGlyphToFont(fontdata, ops[missing[i].id].copy.glyph_index, glyph)) {
+            ReleaseGlyph(glyph);
+            return false;
+        }
+
+        ops[missing[i].id].copy.reserved = glyph;
+    }
+
+    if (all_packed) {
+        return true;
+    }
+
+    // Sort the remaining missing glyphs and try in the next atlas
+    SDL_qsort_r(missing, num_missing, sizeof(*missing), SortMissing, ops);
+    for (int i = 0; i < num_missing; ++i) {
+        if (ops[missing[i].id].copy.reserved) {
+            // No longer missing!
+            num_missing = i;
+            break;
+        }
+    }
+
+    if (!atlas->next) {
+        atlas->next = CreateAtlas(enginedata->renderer);
+        if (!atlas->next) {
+            return false;
+        }
+    }
+    return ResolveMissingGlyphs(enginedata, atlas->next, fontdata, surfaces, ops, num_ops, missing, num_missing);
+}
+
+static bool CreateMissingGlyphs(TTF_RendererTextEngineData *enginedata, TTF_RendererTextEngineFontData *fontdata, TTF_DrawOperation *ops, int num_ops, int num_missing)
+{
+    stbrp_rect *missing = NULL;
+    SDL_Surface **surfaces = NULL;
+    SDL_HashTable *checked = NULL;
+    bool result = false;
+
+    // Build a list of missing glyphs
+    missing = (stbrp_rect *)SDL_calloc(num_missing, sizeof(*missing));
+    if (!missing) {
+        goto done;
+    }
+
+    surfaces = (SDL_Surface **)SDL_calloc(num_ops, sizeof(*surfaces));
+    if (!surfaces) {
+        goto done;
+    }
+
+    checked = SDL_CreateHashTable(NULL, 4, SDL_HashID, SDL_KeyMatchID, NULL, false);
+    if (!checked) {
+        goto done;
+    }
+
+    int missing_index = 0;
+    for (int i = 0; i < num_ops; ++i) {
+        TTF_DrawOperation *op = &ops[i];
+        if (op->cmd == TTF_DRAW_COMMAND_COPY && !op->copy.reserved) {
+            Uint32 glyph_index = op->copy.glyph_index;
+            if (SDL_FindInHashTable(checked, (const void *)(uintptr_t)glyph_index, NULL)) {
+                continue;
+            }
+            if (!SDL_InsertIntoHashTable(checked, (const void *)(uintptr_t)glyph_index, NULL)) {
+                goto done;
+            }
+
+            surfaces[i] = TTF_GetGlyphImageForIndex(fontdata->font, glyph_index);
+            if (!surfaces[i]) {
+                goto done;
+            }
+            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);
+                goto done;
+            }
+
+            missing[missing_index].id = i;
+            missing[missing_index].w = surfaces[i]->w;
+            missing[missing_index].h = surfaces[i]->h;
+            ++missing_index;
+        }
+    }
+    num_missing = missing_index;
+
+    // Sort the glyphs by size
+    SDL_qsort_r(missing, num_missing, sizeof(*missing), SortMissing, ops);
+
+    // Create the texture atlas if necessary
+    if (!enginedata->atlas) {
+        enginedata->atlas = CreateAtlas(enginedata->renderer);
+        if (!enginedata->atlas) {
+            goto done;
+        }
+    }
+
+    if (!ResolveMissingGlyphs(enginedata, enginedata->atlas, fontdata, surfaces, ops, num_ops, missing, num_missing)) {
+        goto done;
+    }
+
+    // Resolve any duplicates
+    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)) {
+                // Something is very wrong...
+                goto done;
+            }
+        }
+    }
+
+    result = true;
+
+done:
+    SDL_DestroyHashTable(checked);
+    if (surfaces) {
+        for (int i = 0; i < num_ops; ++i) {
+            SDL_DestroySurface(surfaces[i]);
+        }
+        SDL_free(surfaces);
+    }
+    SDL_free(missing);
+    return result;
+}
+
+static void DestroyDrawSequence(AtlasDrawSequence *data)
+{
+    if (!data) {
+        return;
+    }
+
+    if (data->next) {
+        DestroyDrawSequence(data->next);
+    }
+    SDL_free(data->texcoords);
+    SDL_free(data->positions);
+    SDL_free(data);
+}
+
+static SDL_Texture *GetOperationTexture(TTF_DrawOperation *op)
+{
+    if (op->cmd == TTF_DRAW_COMMAND_COPY) {
+        AtlasGlyph *glyph = (AtlasGlyph *)op->copy.reserved;
+        return glyph->atlas->texture;
+    }
+    return NULL;
+}
+
+static AtlasDrawSequence *CreateDrawSequence(TTF_DrawOperation *ops, int num_ops)
+{
+    AtlasDrawSequence *sequence = (AtlasDrawSequence *)SDL_calloc(1, sizeof(*sequence));
+    if (!sequence) {
+        return NULL;
+    }
+
+    SDL_assert(num_ops > 0);
+
+    SDL_Texture *texture = GetOperationTexture(&ops[0]);
+    TTF_DrawOperation *end = NULL;
+    for (int i = 1; i < num_ops; ++i) {
+        if (GetOperationTexture(&ops[i]) != texture) {
+            end = &ops[i];
+            break;
+        }
+    }
+
+    int count = (end ? (int)(end - ops) : num_ops);
+    sequence->texture = texture;
+    sequence->num_rects = count;
+    sequence->rects = (SDL_Rect *)SDL_malloc(count * sizeof(*sequence->rects));
+    if (!sequence->rects) {
+        DestroyDrawSequence(sequence);
+        return NULL;
+    }
+
+    for (int i = 0; i < count; ++i) {
+        TTF_DrawOperation *op = &ops[i];
+        SDL_Rect *dst = NULL;
+        switch (op->cmd) {
+        case TTF_DRAW_COMMAND_FILL:
+            dst = &op->fill.rect;
+            break;
+        case TTF_DRAW_COMMAND_COPY:
+            dst = &op->copy.dst;
+            break;
+        default:
+            break;
         }
+        SDL_copyp(&sequence->rects[i], dst);
+    }
 
-        data = CreateGlyphData(texture);
-        if (!data) {
+    if (texture) {
+        AtlasGlyph *glyph;
+
+        sequence->texcoords = (float *)SDL_malloc(count * sizeof(glyph->texcoords));
+        if (!sequence->texcoords) {
+            DestroyDrawSequence(sequence);
             return NULL;
         }
 
-        if (!SDL_InsertIntoHashTable(fontdata->glyphs, (const void *)(uintptr_t)idx, data)) {
-            DestroyGlyphData(data);
+        float *texcoords = sequence->texcoords;
+        for (int i = 0; i < count; ++i) {
+            AtlasGlyph *glyph = (AtlasGlyph *)ops[i].copy.reserved;
+            SDL_memcpy(texcoords, glyph->texcoords, sizeof(glyph->texcoords));
+            texcoords += SDL_arraysize(glyph->texcoords);
+        }
+    }
+
+    sequence->positions = (float *)SDL_malloc(count * 12 * sizeof(*sequence->positions));
+    if (!sequence->positions) {
+        DestroyDrawSequence(sequence);
+        return NULL;
+    }
+
+    if (count < num_ops) {
+        sequence->next = CreateDrawSequence(ops + count, num_ops - count);
+        if (!sequence->next) {
+            DestroyDrawSequence(sequence);
             return NULL;
         }
     }
-    return data;
+    return sequence;
 }
 
 static void DestroyTextData(TTF_RendererTextEngineTextData *data)
@@ -118,69 +600,90 @@ static void DestroyTextData(TTF_RendererTextEngineTextData *data)
         return;
     }
 
-    if (data->ops) {
-        int i;
+    DestroyDrawSequence(data->draw_sequence);
 
-        for (i = 0; i < data->num_ops; ++i) {
-            const TTF_DrawOperation *op = &data->ops[i];
-            if (op->cmd == TTF_DRAW_COMMAND_COPY) {
-                TTF_RendererTextEngineGlyphData *glyph = (TTF_RendererTextEngineGlyphData *)op->copy.reserved;
-                DestroyGlyphData(glyph);
-            }
-        }
-        SDL_free(data->ops);
+    for (int i = 0; i < data->num_glyphs; ++i) {
+        ReleaseGlyph(data->glyphs[i]);
     }
+    SDL_free(data->glyphs);
     SDL_free(data);
 }
 
-static TTF_RendererTextEngineTextData *CreateTextData(SDL_Renderer *renderer, TTF_RendererTextEngineFontData *fontdata, const TTF_DrawOperation *ops, int num_ops)
+static TTF_RendererTextEngineTextData *CreateTextData(TTF_RendererTextEngineData *enginedata, TTF_RendererTextEngineFontData *fontdata, TTF_DrawOperation *ops, int num_ops)
 {
     TTF_RendererTextEngineTextData *data = (TTF_RendererTextEngineTextData *)SDL_calloc(1, sizeof(*data));
     if (!data) {
         return NULL;
     }
 
-    data->ops = (TTF_DrawOperation *)SDL_malloc(num_ops * sizeof(*data->ops));
-    if (!data->ops) {
-        DestroyTextData(data);
-        return NULL;
+    // First, match draw operations to existing glyphs
+    int num_glyphs = 0;
+    int num_missing = 0;
+    for (int i = 0; i < num_ops; ++i) {
+        TTF_DrawOperation *op = &ops[i];
+
+        if (op->cmd != TTF_DRAW_COMMAND_COPY) {
+            continue;
+        }
+
+        ++num_glyphs;
+
+        if (!SDL_FindInHashTable(fontdata->glyphs, (const void *)(uintptr_t)op->copy.glyph_index, (const void **)&op->copy.reserved)) {
+            ++num_missing;
+        }
     }
-    SDL_memcpy(data->ops, ops, num_ops * sizeof(*data->ops));
-    data->num_ops = num_ops;
 
-    for (int i = 0; i < data->num_ops; ++i) {
-        TTF_DrawOperation *op = &data->ops[i];
-        if (op->cmd == TTF_DRAW_COMMAND_COPY) {
-            TTF_RendererTextEngineGlyphData *glyph = GetGlyphData(renderer, fontdata, op->copy.glyph_index);
-            if (!glyph) {
-                DestroyTextData(data);
-                return NULL;
-            }
-            ++glyph->refcount;
-            op->copy.reserved = glyph;
+    // Create any missing glyphs
+    if (num_missing > 0) {
+        if (!CreateMissingGlyphs(enginedata, fontdata, ops, num_ops, num_missing)) {
+            DestroyTextData(data);
+            return NULL;
+        }
+    }
+
+    // Add references to all the glyphs
+    data->glyphs = (AtlasGlyph **)SDL_malloc(num_glyphs * sizeof(*data->glyphs));
+    for (int i = 0; i < num_ops; ++i) {
+        TTF_DrawOperation *op = &ops[i];
+
+        if (op->cmd != TTF_DRAW_COMMAND_COPY) {
+            continue;
         }
+
+        AtlasGlyph *glyph = (AtlasGlyph*)op->copy.reserved;
+        ++glyph->refcount;
+        data->glyphs[data->num_glyphs++] = glyph;
+    }
+
+    // Sort the operations to batch by texture
+    SDL_qsort(ops, num_ops, sizeof(*ops), SortOperations);
+
+    // Create batched draw sequences
+    data->draw_sequence = CreateDrawSequence(ops, num_ops);
+    if (!data->draw_sequence) {
+        DestroyTextData(data);
+        return NULL;
     }
+
     return data;
 }
 
 static void DestroyFontData(TTF_RendererTextEngineFontData *data)
 {
-    if (!data) {
-        return;
-    }
-
-    if (data->glyphs) {
-        SDL_DestroyHashTable(data->glyphs);
+    if (data) {
+        if (data->glyphs) {
+            SDL_DestroyHashTable(data->glyphs);
+        }
+        SDL_free(data);
     }
-    SDL_free(data);
 }
 
-static void NukeGlyphData(const void *key, const void *value, void *unused)
+static void NukeGlyph(const void *key, const void *value, void *unused)
 {
-    TTF_RendererTextEngineGlyphData *data = (TTF_RendererTextEngineGlyphData *)value;
+    AtlasGlyph *glyph = (AtlasGlyph *)value;
     (void)key;
     (void)unused;
-    DestroyGlyphData(data);
+    ReleaseGlyph(glyph);
 }
 
 static TTF_RendererTextEngineFontData *CreateFontData(TTF_RendererTextEngineData *enginedata, TTF_Font *font, Uint32 font_generation)
@@ -191,7 +694,7 @@ static TTF_RendererTextEngineFontData *CreateFontData(TTF_RendererTextEngineData
     }
     data->font = font;
     data->generation = font_generation;
-    data->glyphs = SDL_CreateHashTable(NULL, 4, SDL_HashID, SDL_KeyMatchID, NukeGlyphData, false);
+    data->glyphs = SDL_CreateHashTable(NULL, 4, SDL_HashID, SDL_KeyMatchID, NukeGlyph, false);
     if (!data->glyphs) {
         DestroyFontData(data);
         return NULL;
@@ -207,14 +710,12 @@ static TTF_RendererTextEngineFontData *CreateFontData(TTF_RendererTextEngineData
 
 static void DestroyEngineData(TTF_RendererTextEngineData *data)
 {
-    if (!data) {
-        return;
-    }
-
-    if (data->fonts) {
-        SDL_DestroyHashTable(data->fonts);
+    if (data) {
+        if (data->fonts) {
+            SDL_DestroyHashTable(data->fonts);
+        }
+        SDL_free(data);
     }
-    SDL_free(data);
 }
 
 static void NukeFontData(const void *key, const void *value, void *unused)
@@ -257,7 +758,7 @@ static bool SDLCALL CreateText(void *userdata, TTF_Font *font, Uint32 font_gener
         fontdata->generation = font_generation;
     }
 
-    data = CreateTextData(enginedata->renderer, fontdata, ops, num_ops);
+    data = CreateTextData(enginedata, fontdata, ops, num_ops);
     if (!data) {
         return false;
     }
@@ -275,6 +776,11 @@ static void SDLCALL DestroyText(void *userdata, TTF_Text *text)
 
 TTF_TextEngine *TTF_CreateRendererTextEngine(SDL_Renderer *renderer)
 {
+    if (!renderer) {
+        SDL_InvalidParamError("renderer");
+        return NULL;
+    }
+
     TTF_TextEngine *engine = (TTF_TextEngine *)SDL_malloc(sizeof(*engine));
     if (!engine) {
         return NULL;
@@ -291,42 +797,6 @@ TTF_TextEngine *TTF_CreateRendererTextEngine(SDL_Renderer *renderer)
     return engine;
 }
 
-static void DrawFill(SDL_Renderer *renderer, TTF_Text *text, const TTF_FillOperation *op, float x, float y)
-{
-    SDL_FColor color;
-    SDL_GetRenderDrawColorFloat(renderer, &color.r, &color.g, &color.b, &color.a);
-    SDL_SetRenderDrawColorFloat(renderer, text->color.r, text->color.g, text->color.b, text->color.a);
-
-    SDL_FRect dst;
-    SDL_RectToFRect(&op->rect, &dst);
-    dst.x += x;
-    dst.y += y;
-    SDL_RenderFillRect(renderer, &dst);
-
-    SDL_SetRenderDrawColorFloat(renderer, color.r, color.g, color.b, color.a);
-}
-
-static void DrawCopy(SDL_Renderer *renderer, TTF_Text *text, const TTF_CopyOperation *op, float x, float y)
-{
-    TTF_RendererTextEngineGlyphData *glyph = (TTF_RendererTextEngineGlyphData *)op->reserved;
-
-    if (text->color.r != glyph->color.r ||
-        text->color.g != glyph->color.g ||
-        text->color.b != glyph->color.b ||
-        text->color.a != glyph->color.a) {
-        SDL_SetTextureColorModFloat(glyph->texture, text->color.r, text->color.g, text->color.b);
-        SDL_SetTextureAlphaModFloat(glyph->texture, text->color.a);
-        SDL_copyp(&glyph->color, &text->color);
-    }
-
-    SDL_FRect src, dst;
-    SDL_RectToFRect(&op->src, &src);
-    SDL_RectToFRect(&op->dst, &dst);
-    dst.x += x;
-    dst.y += y;
-    SDL_RenderTexture(renderer, glyph->texture, &src, &dst);
-}
-
 bool TTF_DrawRendererText(TTF_Text *text, float x, float y)
 {
     TTF_RendererTextEngineTextData *data;
@@ -338,19 +808,39 @@ bool TTF_DrawRendererText(TTF_Text *text, float x, float y)
 
     renderer = ((TTF_RendererTextEngineData *)text->internal->engine->userdata)->renderer;
     data = (TTF_RendererTextEngineTextData *)text->internal->textrep;
-
-    for (int i = 0; i < data->num_ops; ++i) {
-        const TTF_DrawOperation *op = &data->ops[i];
-        switch (op->cmd) {
-        case TTF_DRAW_COMMAND_FILL:
-            DrawFill(renderer, text, &op->fill, x, y);
-            break;
-        case TTF_DRAW_COMMAND_COPY:
-            DrawCopy(renderer, text, &op->copy, x, y);
-            break;
-        default:
-            break;
+    AtlasDrawSequence *sequence = data->draw_sequence;
+    while (sequence) {
+        float *position = sequence->positions;
+        for (int i = 0; i < sequence->num_rects; ++i) {
+            const SDL_Rect *dst = &sequence->rects[i];
+            float minx = x + dst->x;
+            float maxx = x + dst->x + dst->w;
+            float miny = y + dst->y;
+            float maxy = y + dst->y + dst->h;
+            position[0] = minx;
+            position[1] = miny;
+            position[2] = maxx;
+            position[3] = miny;
+            position[4] = maxx;
+            position[5] = maxy;
+            position[6] = minx;
+            position[7] = miny;
+            position[8] = maxx;
+            position[9] = maxy;
+            position[10] = minx;
+            position[11] = maxy;
+            position += 12;
         }
+
+        SDL_RenderGeometryRaw(renderer,
+                              sequence->texture,
+                              sequence->positions, 2 * sizeof(float),
+                              &text->color, 0,
+                              sequence->texcoords, 2 * sizeof(float),
+                              sequence->num_rects * 6,
+                              NULL, 0, 0);
+
+        sequence = sequence->next;
     }
     return false;
 }
diff --git a/src/stb_rect_pack.h b/src/stb_rect_pack.h
new file mode 100644
index 00000000..6a633ce6
--- /dev/null
+++ b/src/stb_rect_pack.h
@@ -0,0 +1,623 @@
+// stb_rect_pack.h - v1.01 - public domain - rectangle packing
+// Sean Barrett 2014
+//
+// Useful for e.g. packing rectangular textures into an atlas.
+// Does not do rotation.
+//
+// Before #including,
+//
+//    #define STB_RECT_PACK_IMPLEMENTATION
+//
+// in the file that you want to have the implementation.
+//
+// Not necessarily the awesomest packing method, but better than
+// the totally naive one in stb_truetype (which is primarily what
+// this is meant to replace).
+//
+// Has only had a few tests run, may have issues.
+//
+// More docs to come.
+//
+// No memory allocations; uses qsort() and assert() from stdlib.
+// Can override those by defining STBRP_SORT and STBRP_ASSERT.
+//
+// This library currently uses the Skyline Bottom-Left algorithm.
+//
+// Please note: better rectangle packers are welcome! Please
+// implement them to the same API, but with a different init
+// function.
+//
+// Credits
+//
+//  Library
+//    Sean Barrett
+//  Minor features
+//    Martins Mozeiko
+//    github:IntellectualKitty
+//
+//  Bugfixes / warning fixes
+//    Jeremy Jaussaud
+//    Fabian Giesen
+//
+// Version history:
+//
+//     1.01  (2021-07-11)  always use large rect mode, expose STBRP__MAXVAL in public section
+//     1.00  (2019-02-25)  avoid small space waste; gracefully fail too-wide rectangles
+//     0.99  (2019-02-07)  warning fixes
+//     0.11  (2017-03-03)  return packing success/fail result
+//     0.10  (2016-10-25)  remove cast-away-const to avoid warnings
+//     0.09  (2016-08-27)  fix compiler warnings
+//     0.08  (2015-09-13)  really fix bug with empty rects (w=0 or h=0)
+//     0.07  (2015-09-13)  fix bug with empty rects (w=0 or h=0)
+//     0.06  (2015-04-15)  added STBRP_SORT to allow replacing qsort
+//     0.05:  added STBRP_ASSERT to allow replacing assert
+//     0.04:  fixed minor bug in STBRP_LARGE_RECTS support
+//     0.01:  initial release
+//
+// LICENSE
+//
+//   See end of file for license information.
+
+//////////////////////////////////////////////////////////////////////////////
+//
+//       INCLUDE SECTION
+//
+
+#ifndef STB_INCLUDE_STB_RECT_PACK_H
+#define STB_INCLUDE_STB_RECT_PACK_H
+
+#define STB_RECT_PACK_VERSION  1
+
+#ifdef STBRP_STATIC
+#define STBRP_DEF static
+#else
+#define STBRP_DEF extern
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct stbrp_context stbrp_context;
+typedef struct stbrp_node    stbrp_node;
+typedef struct stbrp_rect    stbrp_rect;
+
+typedef int            stbrp_coord;
+
+#define STBRP__MAXVAL  0x7fffffff
+// Mostly for internal use, but this is the maximum supported coordinate value.
+
+STBRP_DEF int stbrp_pack_rects (stbrp_context *context, stbrp_rect *rects, int num_rects);
+// Assign packed locations to rectangles. The rectangles are of type
+// 'stbrp_rect' defined below, stored in the array 'rects', and there
+// are 'num_rects' many of th

(Patch may be truncated, please check the link at the top of this post.)