From 7df7b48e83f13c1f378390f02360d703ce54f90b Mon Sep 17 00:00:00 2001
From: Ivan Vecera <[EMAIL REDACTED]>
Date: Thu, 18 Jun 2026 03:03:04 +0200
Subject: [PATCH] Add OpenGL text engine and example (#630)
* Add OpenGL text engine
Add a new TextEngine backend for OpenGL that follows the same
atlas-based, batched draw data pattern as the GPU text engine.
The engine loads GL functions via SDL_GL_GetProcAddress at runtime
and handles BGRA/RGBA format differences between desktop GL and ES.
Uses interleaved TTF_GLAtlasDrawVertex (position + texcoord) with
Uint16 indices for GLES 2.0 compatibility. Solid fill sequences
include normalized rectangle coordinates for shader-based edge AA.
* Add testgltext example for the OpenGL text engine
Demonstrates the GL text engine with rotating 3D text, mirroring
the existing testgputext example. All GL functions (1.x and 2.0)
are loaded via SDL_GL_GetProcAddress using a GL_API struct, with
typedefs from SDL_OPENGL_1_FUNCTION_TYPEDEFS.
Uses a single unified branchless GLSL fragment shader that handles
atlas texturing, SDF rendering with outline support, and procedural
edge anti-aliasing for solid fill geometry (underline/strikethrough)
via fwidth()-based screen-space distance computation. Supports both
desktop GL and GLES 2.0 contexts via --gles flag.
---
Android.mk | 1 +
CMakeLists.txt | 6 +
VisualC/SDL_ttf.vcxproj | 1 +
VisualC/SDL_ttf.vcxproj.filters | 3 +
examples/testgltext.c | 517 +++++++++++++
examples/testgputext/SDL_math3d.h | 10 +-
include/SDL3_ttf/SDL_ttf.h | 186 ++++-
src/SDL_gl_textengine.c | 1118 +++++++++++++++++++++++++++++
src/SDL_ttf.exports | 6 +
src/SDL_ttf.sym | 6 +
10 files changed, 1848 insertions(+), 6 deletions(-)
create mode 100644 examples/testgltext.c
create mode 100644 src/SDL_gl_textengine.c
diff --git a/Android.mk b/Android.mk
index 06e1e4e2..074f0d3b 100644
--- a/Android.mk
+++ b/Android.mk
@@ -32,6 +32,7 @@ LOCAL_SRC_FILES := \
src/SDL_ttf.c.neon \
src/SDL_hashtable.c \
src/SDL_hashtable_ttf.c \
+ src/SDL_gl_textengine.c \
src/SDL_gpu_textengine.c \
src/SDL_renderer_textengine.c \
src/SDL_surface_textengine.c
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 6f62e75c..b6f46753 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -140,6 +140,7 @@ endif()
add_library(${sdl3_ttf_target_name}
src/SDL_hashtable.c
src/SDL_hashtable_ttf.c
+ src/SDL_gl_textengine.c
src/SDL_gpu_textengine.c
src/SDL_renderer_textengine.c
src/SDL_surface_textengine.c
@@ -577,15 +578,20 @@ if(SDLTTF_SAMPLES)
add_sdl_ttf_example_executable(showfps examples/showfps.c)
add_sdl_ttf_example_executable(testapp examples/testapp.c)
add_sdl_ttf_example_executable(testgputext examples/testgputext.c)
+ add_sdl_ttf_example_executable(testgltext examples/testgltext.c)
set(OpenGL_GL_PREFERENCE GLVND)
find_package(OpenGL)
if(TARGET OpenGL::OpenGL)
target_compile_definitions(glfont PRIVATE HAVE_OPENGL)
target_link_libraries(glfont PRIVATE OpenGL::OpenGL)
+ target_compile_definitions(testgltext PRIVATE HAVE_OPENGL)
+ target_link_libraries(testgltext PRIVATE OpenGL::OpenGL)
elseif(TARGET OpenGL::GL)
target_compile_definitions(glfont PRIVATE HAVE_OPENGL)
target_link_libraries(glfont PRIVATE OpenGL::GL)
+ target_compile_definitions(testgltext PRIVATE HAVE_OPENGL)
+ target_link_libraries(testgltext PRIVATE OpenGL::GL)
endif()
endif()
diff --git a/VisualC/SDL_ttf.vcxproj b/VisualC/SDL_ttf.vcxproj
index bac1bb4c..6d1894f5 100644
--- a/VisualC/SDL_ttf.vcxproj
+++ b/VisualC/SDL_ttf.vcxproj
@@ -266,6 +266,7 @@
<ClCompile Include="..\external\plutovg\source\plutovg-path.c" />
<ClCompile Include="..\external\plutovg\source\plutovg-rasterize.c" />
<ClCompile Include="..\external\plutovg\source\plutovg-surface.c" />
+ <ClCompile Include="..\src\SDL_gl_textengine.c" />
<ClCompile Include="..\src\SDL_gpu_textengine.c" />
<ClCompile Include="..\src\SDL_hashtable.c" />
<ClCompile Include="..\src\SDL_hashtable_ttf.c" />
diff --git a/VisualC/SDL_ttf.vcxproj.filters b/VisualC/SDL_ttf.vcxproj.filters
index f458bfba..0ee8c720 100644
--- a/VisualC/SDL_ttf.vcxproj.filters
+++ b/VisualC/SDL_ttf.vcxproj.filters
@@ -159,6 +159,9 @@
<ClCompile Include="..\external\harfbuzz\src\harfbuzz.cc">
<Filter>Sources\HarfBuzz</Filter>
</ClCompile>
+ <ClCompile Include="..\src\SDL_gl_textengine.c">
+ <Filter>Sources</Filter>
+ </ClCompile>
<ClCompile Include="..\src\SDL_gpu_textengine.c">
<Filter>Sources</Filter>
</ClCompile>
diff --git a/examples/testgltext.c b/examples/testgltext.c
new file mode 100644
index 00000000..d91b23f6
--- /dev/null
+++ b/examples/testgltext.c
@@ -0,0 +1,517 @@
+/*
+ testgltext: An example of using the SDL_ttf GL text engine with OpenGL.
+ Copyright (C) 2025 Ivan Vecera <ivan@cera.cz>
+
+ 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/SDL.h>
+#include <SDL3/SDL_main.h>
+#include <SDL3_ttf/SDL_ttf.h>
+
+#ifdef HAVE_OPENGL
+
+#define SDL_OPENGL_1_FUNCTION_TYPEDEFS
+#include <SDL3/SDL_opengl.h>
+#include <SDL3/SDL_opengl_glext.h>
+
+#define SDL_MATH_3D_IMPLEMENTATION
+#include "testgputext/SDL_math3d.h"
+
+typedef struct
+{
+ PFNGLATTACHSHADERPROC AttachShader;
+ PFNGLBINDBUFFERPROC BindBuffer;
+ PFNGLBINDTEXTUREPROC BindTexture;
+ PFNGLBLENDFUNCPROC BlendFunc;
+ PFNGLBUFFERDATAPROC BufferData;
+ PFNGLCLEARPROC Clear;
+ PFNGLCLEARCOLORPROC ClearColor;
+ PFNGLCOMPILESHADERPROC CompileShader;
+ PFNGLCREATEPROGRAMPROC CreateProgram;
+ PFNGLCREATESHADERPROC CreateShader;
+ PFNGLDELETEBUFFERSPROC DeleteBuffers;
+ PFNGLDELETEPROGRAMPROC DeleteProgram;
+ PFNGLDELETESHADERPROC DeleteShader;
+ PFNGLDISABLEVERTEXATTRIBARRAYPROC DisableVertexAttribArray;
+ PFNGLDRAWELEMENTSPROC DrawElements;
+ PFNGLENABLEPROC Enable;
+ PFNGLENABLEVERTEXATTRIBARRAYPROC EnableVertexAttribArray;
+ PFNGLGENBUFFERSPROC GenBuffers;
+ PFNGLGETATTRIBLOCATIONPROC GetAttribLocation;
+ PFNGLGETINTEGERVPROC GetIntegerv;
+ PFNGLGETPROGRAMINFOLOGPROC GetProgramInfoLog;
+ PFNGLGETPROGRAMIVPROC GetProgramiv;
+ PFNGLGETSHADERINFOLOGPROC GetShaderInfoLog;
+ PFNGLGETSHADERIVPROC GetShaderiv;
+ PFNGLGETUNIFORMLOCATIONPROC GetUniformLocation;
+ PFNGLLINKPROGRAMPROC LinkProgram;
+ PFNGLSHADERSOURCEPROC ShaderSource;
+ PFNGLUNIFORM1IPROC Uniform1i;
+ PFNGLUNIFORM4FPROC Uniform4f;
+ PFNGLUNIFORMMATRIX4FVPROC UniformMatrix4fv;
+ PFNGLUSEPROGRAMPROC UseProgram;
+ PFNGLVERTEXATTRIBPOINTERPROC VertexAttribPointer;
+} GL_API;
+
+static GL_API gl;
+
+static bool Init_GL_API(GL_API *gl)
+{
+#define LOAD(fntyp, name) \
+ gl->name = (fntyp) SDL_GL_GetProcAddress("gl" #name); \
+ if (!gl->name) { \
+ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to load gl%s", #name); \
+ return false; \
+ }
+ LOAD(PFNGLATTACHSHADERPROC, AttachShader);
+ LOAD(PFNGLBINDBUFFERPROC, BindBuffer);
+ LOAD(PFNGLBINDTEXTUREPROC, BindTexture);
+ LOAD(PFNGLBLENDFUNCPROC, BlendFunc);
+ LOAD(PFNGLBUFFERDATAPROC, BufferData);
+ LOAD(PFNGLCLEARPROC, Clear);
+ LOAD(PFNGLCLEARCOLORPROC, ClearColor);
+ LOAD(PFNGLCOMPILESHADERPROC, CompileShader);
+ LOAD(PFNGLCREATEPROGRAMPROC, CreateProgram);
+ LOAD(PFNGLCREATESHADERPROC, CreateShader);
+ LOAD(PFNGLDELETEBUFFERSPROC, DeleteBuffers);
+ LOAD(PFNGLDELETEPROGRAMPROC, DeleteProgram);
+ LOAD(PFNGLDELETESHADERPROC, DeleteShader);
+ LOAD(PFNGLDISABLEVERTEXATTRIBARRAYPROC, DisableVertexAttribArray);
+ LOAD(PFNGLDRAWELEMENTSPROC, DrawElements);
+ LOAD(PFNGLENABLEPROC, Enable);
+ LOAD(PFNGLENABLEVERTEXATTRIBARRAYPROC, EnableVertexAttribArray);
+ LOAD(PFNGLGENBUFFERSPROC, GenBuffers);
+ LOAD(PFNGLGETATTRIBLOCATIONPROC, GetAttribLocation);
+ LOAD(PFNGLGETINTEGERVPROC, GetIntegerv);
+ LOAD(PFNGLGETPROGRAMINFOLOGPROC, GetProgramInfoLog);
+ LOAD(PFNGLGETPROGRAMIVPROC, GetProgramiv);
+ LOAD(PFNGLGETSHADERINFOLOGPROC, GetShaderInfoLog);
+ LOAD(PFNGLGETSHADERIVPROC, GetShaderiv);
+ LOAD(PFNGLGETUNIFORMLOCATIONPROC, GetUniformLocation);
+ LOAD(PFNGLLINKPROGRAMPROC, LinkProgram);
+ LOAD(PFNGLSHADERSOURCEPROC, ShaderSource);
+ LOAD(PFNGLUNIFORM1IPROC, Uniform1i);
+ LOAD(PFNGLUNIFORM4FPROC, Uniform4f);
+ LOAD(PFNGLUNIFORMMATRIX4FVPROC, UniformMatrix4fv);
+ LOAD(PFNGLUSEPROGRAMPROC, UseProgram);
+ LOAD(PFNGLVERTEXATTRIBPOINTERPROC, VertexAttribPointer);
+#undef LOAD
+ return true;
+}
+
+/* Shader sources */
+
+static const char *vertex_shader_src =
+ "attribute vec2 a_position;\n"
+ "attribute vec2 a_texcoord;\n"
+ "uniform mat4 u_projection;\n"
+ "uniform mat4 u_model;\n"
+ "varying vec2 v_texcoord;\n"
+ "void main() {\n"
+ " v_texcoord = a_texcoord;\n"
+ " gl_Position = u_projection * u_model * vec4(a_position, 0.0, 1.0);\n"
+ "}\n";
+
+static const char *fragment_shader_src =
+ "#ifdef GL_ES\n"
+ "#extension GL_OES_standard_derivatives : enable\n"
+ "precision mediump float;\n"
+ "#endif\n"
+ "uniform sampler2D u_texture;\n"
+ "uniform vec4 u_text_color;\n"
+ "uniform vec4 u_outline_color;\n"
+ "uniform bool u_atlas;\n"
+ "uniform bool u_sdf;\n"
+ "varying vec2 v_texcoord;\n"
+
+ "void main() {\n"
+ " // Atlas & SDF calculations\n"
+ " vec4 texel = texture2D(u_texture, v_texcoord);\n"
+ " float d = texel.a;\n"
+ " float w = fwidth(d);\n"
+
+ " // SDF outer edge of the outline vs. inner text body\n"
+ " float outer = smoothstep(0.50 - w, 0.50 + w, d);\n"
+ " float inner = smoothstep(0.55 - w, 0.55 + w, d);\n"
+
+ " // Color composition for SDF rendering\n"
+ " vec4 color1 = mix(u_outline_color, u_text_color, inner);\n"
+ " color1.a *= outer;\n"
+
+ " // Select between regular texturing and SDF rendering\n"
+ " color1 = mix(u_text_color * texel, color1, float(u_sdf));\n"
+
+ " // Procedural AA rectangle\n"
+ " vec2 dd = (0.5 - abs(v_texcoord - 0.5)) / fwidth(v_texcoord);\n"
+ " float aa = clamp(min(dd.x, dd.y), 0.0, 1.0);\n"
+ " vec4 color2 = vec4(u_text_color.rgb, u_text_color.a * aa);\n"
+
+ " // Final branchless selection\n"
+ " gl_FragColor = mix(color2, color1, float(u_atlas));\n"
+ "}\n";
+
+static GLuint CompileShader(GLenum type, const char *source)
+{
+ GLuint shader = gl.CreateShader(type);
+ GLint status;
+
+ gl.ShaderSource(shader, 1, &source, NULL);
+ gl.CompileShader(shader);
+ gl.GetShaderiv(shader, GL_COMPILE_STATUS, &status);
+ if (!status) {
+ char log[512];
+ gl.GetShaderInfoLog(shader, sizeof(log), NULL, log);
+ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Shader compile error: %s", log);
+ gl.DeleteShader(shader);
+ return 0;
+ }
+ return shader;
+}
+
+static GLuint CreateProgram(const char *vert_src, const char *frag_src)
+{
+ GLuint vert = CompileShader(GL_VERTEX_SHADER, vert_src);
+ if (!vert) {
+ return 0;
+ }
+
+ GLuint frag = CompileShader(GL_FRAGMENT_SHADER, frag_src);
+ if (!frag) {
+ gl.DeleteShader(vert);
+ return 0;
+ }
+
+ GLuint program = gl.CreateProgram();
+ GLint status;
+
+ gl.AttachShader(program, vert);
+ gl.AttachShader(program, frag);
+ gl.LinkProgram(program);
+ gl.DeleteShader(vert);
+ gl.DeleteShader(frag);
+
+ gl.GetProgramiv(program, GL_LINK_STATUS, &status);
+ if (!status) {
+ char log[512];
+ gl.GetProgramInfoLog(program, sizeof(log), NULL, log);
+ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Program link error: %s", log);
+ gl.DeleteProgram(program);
+ return 0;
+ }
+ return program;
+}
+
+typedef struct
+{
+ GLuint program;
+ GLint a_position;
+ GLint a_texcoord;
+ GLint u_projection;
+ GLint u_model;
+ GLint u_text_color;
+ GLint u_outline_color;
+ GLint u_texture;
+ GLint u_atlas;
+ GLint u_sdf;
+} ShaderProgram;
+
+static bool InitProgram(ShaderProgram *sp, const char *vert_src, const char *frag_src)
+{
+ sp->program = CreateProgram(vert_src, frag_src);
+ if (!sp->program) {
+ return false;
+ }
+ sp->a_position = gl.GetAttribLocation(sp->program, "a_position");
+ sp->a_texcoord = gl.GetAttribLocation(sp->program, "a_texcoord");
+ sp->u_projection = gl.GetUniformLocation(sp->program, "u_projection");
+ sp->u_model = gl.GetUniformLocation(sp->program, "u_model");
+ sp->u_text_color = gl.GetUniformLocation(sp->program, "u_text_color");
+ sp->u_outline_color = gl.GetUniformLocation(sp->program, "u_outline_color");
+ sp->u_texture = gl.GetUniformLocation(sp->program, "u_texture");
+ sp->u_atlas = gl.GetUniformLocation(sp->program, "u_atlas");
+ sp->u_sdf = gl.GetUniformLocation(sp->program, "u_sdf");
+ return true;
+}
+
+static void DrawGLText(ShaderProgram *sp, bool use_sdf, GLuint vbo, GLuint ebo,
+ TTF_GLAtlasDrawSequence *sequence,
+ SDL_Mat4X4 *projection, SDL_Mat4X4 *model,
+ SDL_FColor *color, SDL_FColor *out_color)
+{
+ gl.UseProgram(sp->program);
+ gl.UniformMatrix4fv(sp->u_projection, 1, GL_FALSE, &projection->m[0][0]);
+ gl.UniformMatrix4fv(sp->u_model, 1, GL_FALSE, &model->m[0][0]);
+ gl.Uniform4f(sp->u_text_color, color->r, color->g, color->b, color->a);
+ gl.Uniform4f(sp->u_outline_color, out_color->r, out_color->g, out_color->b, out_color->a);
+ gl.Uniform1i(sp->u_sdf, use_sdf);
+
+ for (TTF_GLAtlasDrawSequence *seq = sequence; seq; seq = seq->next) {
+ gl.Uniform1i(sp->u_atlas, seq->atlas_texture != 0);
+
+ if (seq->atlas_texture) {
+ gl.BindTexture(GL_TEXTURE_2D, seq->atlas_texture);
+ gl.Uniform1i(sp->u_texture, 0);
+ }
+
+ gl.BindBuffer(GL_ARRAY_BUFFER, vbo);
+ gl.BufferData(GL_ARRAY_BUFFER, seq->num_vertices * sizeof(TTF_GLAtlasDrawVertex), seq->vertices, GL_STREAM_DRAW);
+
+ gl.BindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
+ gl.BufferData(GL_ELEMENT_ARRAY_BUFFER, seq->num_indices * sizeof(Uint16), seq->indices, GL_STREAM_DRAW);
+
+ GLsizei stride = sizeof(TTF_GLAtlasDrawVertex);
+ gl.EnableVertexAttribArray(sp->a_position);
+ gl.VertexAttribPointer(sp->a_position, 2, GL_FLOAT, GL_FALSE, stride, (void *)0);
+ gl.EnableVertexAttribArray(sp->a_texcoord);
+ gl.VertexAttribPointer(sp->a_texcoord, 2, GL_FLOAT, GL_FALSE, stride, (void *)sizeof(SDL_FPoint));
+
+ gl.DrawElements(GL_TRIANGLES, seq->num_indices, GL_UNSIGNED_SHORT, NULL);
+
+ gl.DisableVertexAttribArray(sp->a_position);
+ gl.DisableVertexAttribArray(sp->a_texcoord);
+ }
+}
+
+int main(int argc, char *argv[])
+{
+ const char *font_filename = NULL;
+ bool use_SDF = false;
+ bool use_GLES = false;
+ int style;
+
+ (void)argc;
+ for (int i = 1; argv[i]; ++i) {
+ if (SDL_strcasecmp(argv[i], "--sdf") == 0) {
+ use_SDF = true;
+ } else if (SDL_strcasecmp(argv[i], "--gles") == 0) {
+ use_GLES = true;
+ } else if (*argv[i] == '-') {
+ break;
+ } else {
+ font_filename = argv[i];
+ break;
+ }
+ }
+ if (!font_filename) {
+ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Usage: testgltext [--sdf] [--gles] FONT_FILENAME");
+ return 2;
+ }
+
+ if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS)) {
+ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_Init failed: %s", SDL_GetError());
+ return 1;
+ }
+
+ if (use_GLES) {
+ SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
+ SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2);
+ SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
+ }
+
+ SDL_Window *window = SDL_CreateWindow("GL text test", 800, 600, SDL_WINDOW_OPENGL);
+ if (!window) {
+ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateWindow failed: %s", SDL_GetError());
+ SDL_Quit();
+ return 1;
+ }
+
+ SDL_GLContext gl_context = SDL_GL_CreateContext(window);
+ if (!gl_context) {
+ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_GL_CreateContext failed: %s", SDL_GetError());
+ SDL_DestroyWindow(window);
+ SDL_Quit();
+ return 1;
+ }
+
+ SDL_Log("Using OpenGL%s", use_GLES ? " ES" : "");
+
+ if (!Init_GL_API(&gl)) {
+ SDL_GL_DestroyContext(gl_context);
+ SDL_DestroyWindow(window);
+ SDL_Quit();
+ return 1;
+ }
+
+ ShaderProgram prog;
+ if (!InitProgram(&prog, vertex_shader_src, fragment_shader_src)) {
+ SDL_GL_DestroyContext(gl_context);
+ SDL_DestroyWindow(window);
+ SDL_Quit();
+ return 1;
+ }
+
+ if (!SDL_GL_SetSwapInterval(-1)) {
+ SDL_GL_SetSwapInterval(1);
+ }
+
+ GLuint vbo, ebo;
+ gl.GenBuffers(1, &vbo);
+ gl.GenBuffers(1, &ebo);
+
+ gl.Enable(GL_BLEND);
+ gl.BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+ if (!TTF_Init()) {
+ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "TTF_Init failed: %s", SDL_GetError());
+ SDL_GL_DestroyContext(gl_context);
+ SDL_DestroyWindow(window);
+ SDL_Quit();
+ return 1;
+ }
+
+ TTF_Font *font = TTF_OpenFont(font_filename, 50);
+ if (!font) {
+ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "TTF_OpenFont failed: %s", SDL_GetError());
+ TTF_Quit();
+ SDL_GL_DestroyContext(gl_context);
+ SDL_DestroyWindow(window);
+ SDL_Quit();
+ return 1;
+ }
+
+ SDL_Log("SDF %s", use_SDF ? "enabled" : "disabled");
+ TTF_SetFontSDF(font, use_SDF);
+ TTF_SetFontWrapAlignment(font, TTF_HORIZONTAL_ALIGN_CENTER);
+
+ TTF_TextEngine *engine = TTF_CreateGLTextEngine();
+ if (!engine) {
+ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "TTF_CreateGLTextEngine failed: %s", SDL_GetError());
+ TTF_CloseFont(font);
+ TTF_Quit();
+ SDL_GL_DestroyContext(gl_context);
+ SDL_DestroyWindow(window);
+ SDL_Quit();
+ return 1;
+ }
+
+ char str[] = " \nSDL is cool";
+ TTF_Text *text = TTF_CreateText(engine, font, str, 0);
+ if (!text) {
+ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "TTF_CreateText failed: %s", SDL_GetError());
+ TTF_DestroyGLTextEngine(engine);
+ TTF_CloseFont(font);
+ TTF_Quit();
+ SDL_GL_DestroyContext(gl_context);
+ SDL_DestroyWindow(window);
+ SDL_Quit();
+ return 1;
+ }
+
+ SDL_Mat4X4 projection = SDL_MatrixPerspective(SDL_PI_F / 2.0f, 800.0f / 600.0f, 0.1f, 100.0f);
+
+ SDL_FColor color = { 1.0f, 1.0f, 0.0f, 1.0f };
+ SDL_FColor out_color = { 0.0f, 0.0f, 0.0f, 1.0f };
+ bool running = true;
+ Uint64 last_ticks = 0;
+
+ while (running) {
+ SDL_Event event;
+ while (SDL_PollEvent(&event)) {
+ switch (event.type) {
+ case SDL_EVENT_KEY_UP:
+ switch (event.key.key) {
+ case SDLK_S:
+ style = TTF_GetFontStyle(font);
+ if (style & TTF_STYLE_STRIKETHROUGH) {
+ style &= ~TTF_STYLE_STRIKETHROUGH;
+ } else {
+ style |= TTF_STYLE_STRIKETHROUGH;
+ }
+ TTF_SetFontStyle(font, style);
+ break;
+
+ case SDLK_U:
+ style = TTF_GetFontStyle(font);
+ if (style & TTF_STYLE_UNDERLINE) {
+ style &= ~TTF_STYLE_UNDERLINE;
+ } else {
+ style |= TTF_STYLE_UNDERLINE;
+ }
+ TTF_SetFontStyle(font, style);
+ break;
+
+ case SDLK_ESCAPE:
+ running = false;
+ break;
+
+ default:
+ break;
+ }
+ break;
+ case SDL_EVENT_QUIT:
+ running = false;
+ break;
+ }
+ }
+
+ Uint64 cur_ticks = SDL_GetTicks();
+ if (cur_ticks - last_ticks >= 250) {
+ for (int i = 0; i < 5; i++) {
+ str[i] = 65 + SDL_rand(26);
+ }
+ TTF_SetTextString(text, str, 0);
+ last_ticks = cur_ticks;
+ }
+
+ int tw, th;
+ TTF_GetTextSize(text, &tw, &th);
+
+ float rot_angle = SDL_fmodf(SDL_GetTicks() / 1000.0f, 2 * SDL_PI_F);
+
+ SDL_Mat4X4 model = SDL_MatrixIdentity();
+ model = SDL_MatrixMultiply(model, SDL_MatrixTranslation((SDL_Vec3){ 0.0f, 0.0f, -80.0f }));
+ model = SDL_MatrixMultiply(model, SDL_MatrixScaling((SDL_Vec3){ 0.3f, 0.3f, 0.3f }));
+ model = SDL_MatrixMultiply(model, SDL_MatrixRotationY(rot_angle));
+ model = SDL_MatrixMultiply(model, SDL_MatrixTranslation((SDL_Vec3){ -tw / 2.0f, th / 2.0f, 0.0f }));
+
+ gl.ClearColor(0.3f, 0.4f, 0.5f, 1.0f);
+ gl.Clear(GL_COLOR_BUFFER_BIT);
+
+ TTF_GLAtlasDrawSequence *sequence = TTF_GetGLTextDrawData(text);
+ DrawGLText(&prog, use_SDF, vbo, ebo, sequence, &projection, &model, &color, &out_color);
+
+ SDL_GL_SwapWindow(window);
+ }
+
+ gl.DeleteBuffers(1, &vbo);
+ gl.DeleteBuffers(1, &ebo);
+ gl.DeleteProgram(prog.program);
+
+ TTF_DestroyText(text);
+ TTF_DestroyGLTextEngine(engine);
+ TTF_CloseFont(font);
+ TTF_Quit();
+
+ SDL_GL_DestroyContext(gl_context);
+ SDL_DestroyWindow(window);
+ SDL_Quit();
+
+ return 0;
+}
+
+#else /* HAVE_OPENGL */
+
+int main(int argc, char *argv[])
+{
+ (void)argc;
+ (void)argv;
+ SDL_Log("No OpenGL support on this system");
+ return 1;
+}
+
+#endif /* HAVE_OPENGL */
diff --git a/examples/testgputext/SDL_math3d.h b/examples/testgputext/SDL_math3d.h
index 60116abb..3ef8b751 100644
--- a/examples/testgputext/SDL_math3d.h
+++ b/examples/testgputext/SDL_math3d.h
@@ -80,7 +80,7 @@ static inline SDL_Mat4X4 SDL_MatrixRotationY(float angle);
static inline SDL_Mat4X4 SDL_MatrixRotationZ(float angle);
static inline SDL_Mat4X4 SDL_MatrixOrtho(float left, float right, float bottom, float top, float back, float front);
-static inline SDL_Mat4X4 SDL_MatrixPerspective(float fovy, float aspect_ratio, float near, float far);
+static inline SDL_Mat4X4 SDL_MatrixPerspective(float fovy, float aspect_ratio, float nearval, float farval);
static inline SDL_Mat4X4 SDL_MatrixLookAt(SDL_Vec3 pos, SDL_Vec3 target, SDL_Vec3 up);
@@ -259,9 +259,9 @@ static inline SDL_Mat4X4 SDL_MatrixRotationZ(float angle)
);
}
-static inline SDL_Mat4X4 SDL_MatrixOrtho(float left, float right, float bottom, float top, float near, float far)
+static inline SDL_Mat4X4 SDL_MatrixOrtho(float left, float right, float bottom, float top, float nearval, float farval)
{
- float l = left, r = right, b = bottom, t = top, n = near, f = far;
+ float l = left, r = right, b = bottom, t = top, n = nearval, f = farval;
float dx = -(r + l) / (r - l);
float dy = -(t + b) / (t - b);
@@ -275,9 +275,9 @@ static inline SDL_Mat4X4 SDL_MatrixOrtho(float left, float right, float bottom,
);
}
-static inline SDL_Mat4X4 SDL_MatrixPerspective(float fovy, float aspect_ratio, float near, float far)
+static inline SDL_Mat4X4 SDL_MatrixPerspective(float fovy, float aspect_ratio, float nearval, float farval)
{
- float n = near; float f = far;
+ float n = nearval; float f = farval;
float t = SDL_tanf(fovy/2.0f) * n;
float b = -t;
float r = t * aspect_ratio;
diff --git a/include/SDL3_ttf/SDL_ttf.h b/include/SDL3_ttf/SDL_ttf.h
index f0b54f9c..ae262cc5 100644
--- a/include/SDL3_ttf/SDL_ttf.h
+++ b/include/SDL3_ttf/SDL_ttf.h
@@ -1733,12 +1733,13 @@ extern SDL_DECLSPEC SDL_Surface * SDLCALL TTF_RenderGlyph_LCD(TTF_Font *font, Ui
* to perform customize rendering with text objects. See
* <SDL3_ttf/SDL_textengine.h> for details.
*
- * There are three text engines provided with the library:
+ * There are four text engines provided with the library:
*
* - Drawing to an SDL_Surface, created with TTF_CreateSurfaceTextEngine()
* - Drawing with an SDL 2D renderer, created with
* TTF_CreateRendererTextEngine()
* - Drawing with the SDL GPU API, created with TTF_CreateGPUTextEngine()
+ * - Drawing with OpenGL, created with TTF_CreateGLTextEngine()
*
* \since This struct is available since SDL_ttf 3.0.0.
*/
@@ -2081,6 +2082,189 @@ extern SDL_DECLSPEC void SDLCALL TTF_SetGPUTextEngineWinding(TTF_TextEngine *eng
*/
extern SDL_DECLSPEC TTF_GPUTextEngineWinding SDLCALL TTF_GetGPUTextEngineWinding(const TTF_TextEngine *engine);
+/**
+ * Create a text engine for drawing text with OpenGL.
+ *
+ * The caller is responsible for ensuring the correct OpenGL context is current
+ * when calling this function and when using the resulting text engine.
+ *
+ * The GL text engine and all text created with it become invalid if the
+ * OpenGL context is destroyed. Destroy the engine before destroying the
+ * context.
+ *
+ * \returns a TTF_TextEngine object or NULL on failure; call SDL_GetError()
+ * for more information.
+ *
+ * \threadsafety This function should be called on the thread that created the
+ * OpenGL context.
+ *
+ * \since This function is available since SDL_ttf 3.3.0.
+ *
+ * \sa TTF_CreateGLTextEngineWithProperties
+ * \sa TTF_DestroyGLTextEngine
+ * \sa TTF_GetGLTextDrawData
+ */
+extern SDL_DECLSPEC TTF_TextEngine * SDLCALL TTF_CreateGLTextEngine(void);
+
+/**
+ * Create a text engine for drawing text with OpenGL, with extra properties.
+ *
+ * The caller is responsible for ensuring the correct OpenGL context is current
+ * when calling this function and when using the resulting text engine.
+ *
+ * The following properties are supported:
+ *
+ * - `TTF_PROP_GL_TEXT_ENGINE_ATLAS_TEXTURE_SIZE_NUMBER`: the size of the
+ * texture atlas in pixels, defaults to 1024.
+ *
+ * \param props the properties to use.
+ * \returns a TTF_TextEngine object or NULL on failure; call SDL_GetError()
+ * for more information.
+ *
+ * \threadsafety This function should be called on the thread that created the
+ * OpenGL context.
+ *
+ * \since This function is available since SDL_ttf 3.3.0.
+ *
+ * \sa TTF_CreateGLTextEngine
+ * \sa TTF_DestroyGLTextEngine
+ * \sa TTF_GetGLTextDrawData
+ */
+extern SDL_DECLSPEC TTF_TextEngine * SDLCALL TTF_CreateGLTextEngineWithProperties(SDL_PropertiesID props);
+
+#define TTF_PROP_GL_TEXT_ENGINE_ATLAS_TEXTURE_SIZE_NUMBER "SDL_ttf.gl_text_engine.create.atlas_texture_size"
+
+/**
+ * A vertex in the draw data returned by TTF_GetGLTextDrawData.
+ *
+ * \since This struct is available since SDL_ttf 3.3.0.
+ *
+ * \sa TTF_GLAtlasDrawSequence
+ * \sa TTF_GetGLTextDrawData
+ */
+typedef struct TTF_GLAtlasDrawVertex
+{
+ SDL_FPoint position; /**< Vertex position */
+ SDL_FPoint texcoord; /**< Texture coordinate, or normalized rectangle coordinate for solid fill */
+} TTF_GLAtlasDrawVertex;
+
+/**
+ * A draw sequence in the linked list returned by TTF_GetGLTextDrawData.
+ *
+ * Each sequence groups primitives that share the same atlas texture
+ * and image type, allowing them to be drawn in a single draw call.
+ *
+ * \since This struct is available since SDL_ttf 3.3.0.
+ *
+ * \sa TTF_GLAtlasDrawVertex
+ * \sa TTF_GetGLTextDrawData
+ */
+typedef struct TTF_GLAtlasDrawSequence
+{
+ unsigned int atlas_texture; /**< OpenGL texture name (same as GLuint), or 0 for solid fill */
+ TTF_GLAtlasDrawVertex *vertices; /**< An array of interleaved vertex data */
+ int num_vertices; /**< Number of vertices */
+ Uint16 *indices; /**< An array of indices into the 'vertices' array */
+ int num_indices; /**< Number of indices */
+ TTF_ImageType image_type; /**< The image type of this draw sequence */
+
+ struct TTF_GLAtlasDrawSequence *next; /**< The next sequence (will be NULL in case of the last sequence) */
+} TTF_GLAtlasDrawSequence;
+
+/**
+ * Get the geometry data needed for drawing the text.
+ *
+ * `text` must have been created using a TTF_TextEngine from
+ * TTF_CreateGLTextEngine().
+ *
+ * The positive X-axis is taken towards the right and the positive Y-axis is
+ * taken upwards for both the vertex and the texture coordinates, i.e, it
+ * follows the same convention used by the OpenGL API. If you want to use a
+ * different coordinate system you will need to transform the vertices
+ * yourself.
+ *
+ * If the text looks blocky use linear filtering.
+ *
+ * \param text the text to draw.
+ * \returns a NULL terminated linked list of TTF_GLAtlasDrawSequence objects
+ * or NULL if the passed text is empty or in case of failure; call
+ * SDL_GetError() for more information.
+ *
+ * \threadsafety This function should be called on the thread that created the
+ * text.
+ *
+ * \since This function is available since SDL_ttf 3.3.0.
+ *
+ * \sa TTF_CreateGLTextEngine
+ * \sa TTF_CreateText
+ */
+extern SDL_DECLSPEC TTF_GLAtlasDrawSequence * SDLCALL TTF_GetGLTextDrawData(TTF_Text *text);
+
+/**
+ * Destroy a text engine created for drawing text with OpenGL.
+ *
+ * All text created by this engine should be destroyed before calling this
+ * function.
+ *
+ * \param engine a TTF_TextEngine object created with
+ * TTF_CreateGLTextEngine().
+ *
+ * \threadsafety This function should be called on the thread that created the
+ * engine.
+ *
+ * \since This function is available since SDL_ttf 3.3.0.
+ *
+ * \sa TTF_CreateGLTextEngine
+ */
+extern SDL_DECLSPEC void SDLCALL TTF_DestroyGLTextEngine(TTF_TextEngine *engine);
+
+/**
+ * The winding order of the vertices returned by TTF_GetGLTextDrawData.
+ *
+ * \since This enum is available since SDL_ttf 3.3.0.
+ */
+typedef enum TTF_GLTextEngineWinding
+{
+ TTF_GL_TEXTENGINE_WINDING_INVALID = -1,
+ TTF_GL_TEXTENGINE_WINDING_CLOCKWISE,
+ TTF_GL_TEXTENGINE_WINDING_COUNTER_CLOCKWISE
+} TTF_GLTextEngineWinding;
+
+/**
+ * Sets the winding order of the vertices returned by TTF_GetGLTextDrawData
+ * for a particular GL text engine.
+ *
+ * \param engine a TTF_TextEngine object created with
+ * TTF_CreateGLTextEngine().
+ * \param winding the new winding order option.
+ *
(Patch may be truncated, please check the link at the top of this post.)