diff options
author | Leo Tenenbaum <pommicket@gmail.com> | 2020-11-23 18:29:58 -0500 |
---|---|---|
committer | Leo Tenenbaum <pommicket@gmail.com> | 2020-11-23 18:29:58 -0500 |
commit | 55d0ece0a9072ca409bdf6ff2f3b6d0b268e2952 (patch) | |
tree | 5df0cbfacf3d9442ddb34fdf5fb3b59f4e74d82e | |
parent | 5e458dff3bcc832b0b28d83bd3ef482174d1dc09 (diff) |
unicode text rendering working
-rw-r--r-- | Makefile | 6 | ||||
-rw-r--r-- | base.h | 2 | ||||
-rw-r--r-- | lib/stb_truetype.h (renamed from stb_truetype.h) | 0 | ||||
-rw-r--r-- | main.c | 30 | ||||
-rw-r--r-- | text.c | 230 | ||||
-rw-r--r-- | text.h | 24 | ||||
-rw-r--r-- | valgrind_suppresions.txt | 70 |
7 files changed, 307 insertions, 55 deletions
@@ -4,5 +4,7 @@ LIBS=-lSDL2 -lGL -ldl -lm DEBUG_CFLAGS=$(ALL_CFLAGS) -DDEBUG -O0 -g ted: *.[ch] text.o $(CC) main.c text.o -o $@ $(DEBUG_CFLAGS) $(LIBS) -%.o: %.c - $(CC) $< -c -o $@ $(DEBUG_CFLAGS) +text.o: text.c text.h base.h lib/stb_truetype.h + $(CC) text.c -c -o $@ $(DEBUG_CFLAGS) +clean: + rm -f ted *.o @@ -40,7 +40,7 @@ typedef unsigned long ulong; #if DEBUG #if __unix__ -#define debug_println printf +#define debug_println(...) printf(__VA_ARGS__), printf("\n") #else // __unix__ static void debug_println(char const *fmt, ...) { char buf[256]; diff --git a/stb_truetype.h b/lib/stb_truetype.h index 62595a1..62595a1 100644 --- a/stb_truetype.h +++ b/lib/stb_truetype.h @@ -3,6 +3,7 @@ no_warn_start #include <SDL2/SDL.h> no_warn_end #include <GL/gl.h> +#include <locale.h> #include "text.h" static void die(char const *fmt, ...) { @@ -23,7 +24,8 @@ static void die(char const *fmt, ...) { int main(void) { - if (SDL_Init(SDL_INIT_VIDEO) < 0) + setlocale(LC_ALL, ""); // allow unicode + if (SDL_Init(SDL_INIT_VIDEO|SDL_INIT_TIMER) < 0) die("%s", SDL_GetError()); SDL_Window *window = SDL_CreateWindow("ted", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 1280, 720, SDL_WINDOW_SHOWN|SDL_WINDOW_OPENGL); @@ -39,7 +41,7 @@ int main(void) { SDL_GL_SetSwapInterval(1); // vsync - Font *font = text_font_load("assets/font.ttf", 12); + Font *font = text_font_load("assets/font.ttf", 24); if (!font) { die("Couldn't load font: %s", text_get_err()); } @@ -55,11 +57,33 @@ int main(void) { } } - glClearColor(0,0,0,1); + int window_width = 0, window_height = 0; + SDL_GetWindowSize(window, &window_width, &window_height); + + // set up GL + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glViewport(0, 0, window_width, window_height); + glMatrixMode(GL_MODELVIEW); + glLoadIdentity(); + glOrtho(0, window_width, 0, window_height, -1, +1); + glClearColor(0, 0, 0, 1); glClear(GL_COLOR_BUFFER_BIT); + glColor3f(1,1,1); + text_render(font, u8"hőello☐øλㄔ☺☹", 50, 50); + if (text_has_err()) { + printf("Text error: %s\n", text_get_err()); + break; + } + SDL_GL_SwapWindow(window); } + SDL_GL_DeleteContext(glctx); + SDL_DestroyWindow(window); + SDL_Quit(); + text_font_free(font); + return 0; } @@ -3,25 +3,34 @@ #define STB_TRUETYPE_IMPLEMENTATION #define STBTT_STATIC no_warn_start -#include "stb_truetype.h" +#include "lib/stb_truetype.h" no_warn_end #include <stdarg.h> #include <stdlib.h> #include <GL/gl.h> +#define UNICODE_CODE_POINTS 0x110000 // number of Unicode code points +// We split up code points into a bunch of pages, so we don't have to load all of the font at +// once into one texture. +#define CHAR_PAGE_SIZE 2048 +#define CHAR_PAGE_COUNT UNICODE_CODE_POINTS / CHAR_PAGE_SIZE + struct Font { float char_height; - GLuint texture; - u32 nchars; - stbtt_bakedchar chars[]; + GLuint textures[CHAR_PAGE_COUNT]; + int tex_widths[CHAR_PAGE_COUNT], tex_heights[CHAR_PAGE_COUNT]; + stbtt_bakedchar *char_pages[CHAR_PAGE_COUNT]; // character pages. NULL if the page hasn't been loaded yet. + // TTF data (i.e. the contents of the TTF file) + u8 *ttf_data; + int curr_page; }; static char text_err[200]; -static void text_clear_err(void) { +void text_clear_err(void) { text_err[0] = '\0'; } -static bool text_has_err(void) { +bool text_has_err(void) { return text_err[0] != '\0'; } @@ -38,9 +47,57 @@ static void text_set_err(char const *fmt, ...) { } } +static void text_load_char_page(Font *font, int page) { + if (font->char_pages[page]) { + // already loaded + return; + } + font->char_pages[page] = calloc(CHAR_PAGE_SIZE, sizeof *font->char_pages[page]); + for (int bitmap_width = 128, bitmap_height = 128; bitmap_width <= 4096; bitmap_width *= 2, bitmap_height *= 2) { + u8 *bitmap = calloc((size_t)bitmap_width, (size_t)bitmap_height); + if (bitmap) { + int err = stbtt_BakeFontBitmap(font->ttf_data, 0, font->char_height, bitmap, + bitmap_width, bitmap_height, page * CHAR_PAGE_SIZE, CHAR_PAGE_SIZE, font->char_pages[page]); + if (err > 0) { + // font converted to bitmap successfully. + GLuint texture = 0; + glGenTextures(1, &texture); + glBindTexture(GL_TEXTURE_2D, texture); + glTexImage2D(GL_TEXTURE_2D, 0, GL_ALPHA, bitmap_width, bitmap_height, 0, GL_ALPHA, GL_UNSIGNED_BYTE, bitmap); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + #if DEBUG + debug_println("Loaded font page %d with %dx%d bitmap as texture %u.", page, bitmap_width, bitmap_height, texture); + #endif + font->textures[page] = texture; + font->tex_widths[page] = bitmap_width; + font->tex_heights[page] = bitmap_height; + GLenum glerr = glGetError(); + if (glerr) { + text_set_err("Couldn't create texture for font (GL error %u).", glerr); + if (texture) glDeleteTextures(1, &texture); + break; + } + } + } else { + text_set_err("Not enough memory for font bitmap."); + } + free(bitmap); + if (font->textures[page]) { // if font loaded successfully + break; + } + } + if (!font->textures[page] && !text_has_err()) { + text_set_err("Couldn't convert font to bitmap."); + } + if (text_has_err()) { + free(font->char_pages[page]); + font->char_pages[page] = NULL; + } +} + Font *text_font_load(char const *ttf_filename, float font_size) { Font *font = NULL; - u32 nchars = 128; FILE *ttf_file = fopen(ttf_filename, "rb"); text_clear_err(); @@ -48,54 +105,25 @@ Font *text_font_load(char const *ttf_filename, float font_size) { if (ttf_file) { fseek(ttf_file, 0, SEEK_END); u32 file_size = (u32)ftell(ttf_file); + fseek(ttf_file, 0, SEEK_SET); if (file_size < (50UL<<20)) { // fonts aren't usually bigger than 50 MB u8 *file_data = calloc(1, file_size); - font = calloc(1, sizeof *font + nchars * sizeof *font->chars); + font = calloc(1, sizeof *font); if (file_data && font) { - if (fread(file_data, 1, file_size, ttf_file) == file_size) { - font->nchars = nchars; + size_t bytes_read = fread(file_data, 1, file_size, ttf_file); + if (bytes_read == file_size) { font->char_height = font_size; - - for (int bitmap_width = 256, bitmap_height = 256; bitmap_width <= 4096; bitmap_width *= 2, bitmap_height *= 2) { - u8 *bitmap = calloc((size_t)bitmap_width, (size_t)bitmap_height); - if (bitmap) { - int err = stbtt_BakeFontBitmap(file_data, 0, font->char_height, bitmap, - bitmap_width, bitmap_height, 0, (int)font->nchars, font->chars); - if (err > 0) { - // font converted to bitmap successfully. - GLuint texture = 0; - glGenTextures(1, &texture); - glBindTexture(GL_TEXTURE_2D, texture); - glTexImage2D(GL_TEXTURE_2D, 0, GL_ALPHA, bitmap_width, bitmap_height, 0, GL_ALPHA, GL_UNSIGNED_BYTE, bitmap); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - #if DEBUG - debug_println("Loaded font %s with %dx%d bitmap.", ttf_filename, bitmap_width, bitmap_height); - #endif - font->texture = texture; - if (glGetError()) { - text_set_err("Couldn't create texture for font."); - } - } - } else { - text_set_err("Not enough memory for font bitmap."); - } - free(bitmap); - if (font->texture) { // if font loaded successfully - break; - } - } - if (!font->texture && !text_has_err()) { - text_set_err("Couldn't convert font to bitmap."); - } + font->ttf_data = file_data; + text_load_char_page(font, 0); // load page with Latin text, etc. + font->curr_page = -1; } else { - text_set_err("Couldn't read font file.", ttf_filename); + text_set_err("Couldn't read font file."); } } else { text_set_err("Not enough memory for font."); } - free(file_data); if (text_has_err()) { + free(file_data); free(font); font = NULL; } @@ -109,11 +137,117 @@ Font *text_font_load(char const *ttf_filename, float font_size) { return font; } -#if 0 +typedef struct { + float x, y; +} TextRenderState; + +static void text_render_with_page(Font *font, int page) { + if (font->curr_page != page) { + if (font->curr_page != -1) { + // we were rendering chars from another page. + glEnd(); // stop doing that + } + text_load_char_page(font, page); // load the page if necessary + glEnable(GL_TEXTURE_2D); + glBindTexture(GL_TEXTURE_2D, font->textures[page]); + glBegin(GL_QUADS); + font->curr_page = page; + } +} + +void text_chars_begin(Font *font) { + text_render_with_page(font, 0); // start by loading Latin text +} + +void text_chars_end(Font *font) { + glEnd(); + glDisable(GL_TEXTURE_2D); + font->curr_page = -1; +} + +static void text_render_char_internal(Font *font, char32_t c, TextRenderState *state) { + if (c >= 0x30000 && c < 0xE0000){ + // these Unicode code points are currently unassigned. replace them with ☐. + // (specifically, we don't want to use extra memory for pages which + // won't even have any valid characters in them) + c = 0x2610; + } + if (c >= UNICODE_CODE_POINTS) c = 0x2610; // code points this big should never appear in valid Unicode + uint page = c / CHAR_PAGE_SIZE; + uint index = c % CHAR_PAGE_SIZE; + text_render_with_page(font, (int)page); + stbtt_bakedchar *char_data = font->char_pages[page]; + if (char_data) { // if page was successfully loaded + stbtt_aligned_quad q = {0}; + // because stb_truetype uses down is positive, we need to negate the y + // coordinate, pass it into the function, then negate it back. + state->y = -state->y; + stbtt_GetBakedQuad(char_data, font->tex_widths[page], font->tex_heights[page], + (int)index, &state->x, &state->y, &q, 1); + state->y = -state->y; + glTexCoord2f(q.s0,q.t1); glVertex2f(q.x0,-q.y1); + glTexCoord2f(q.s1,q.t1); glVertex2f(q.x1,-q.y1); + glTexCoord2f(q.s1,q.t0); glVertex2f(q.x1,-q.y0); + glTexCoord2f(q.s0,q.t0); glVertex2f(q.x0,-q.y0); + } +} + +void text_render_char(Font *font, char32_t c, float *x, float *y) { + TextRenderState state = {*x, *y}; + text_render_char_internal(font, c, &state); + *x = state.x; + *y = state.y; +} + +static void text_render_internal(Font *font, char const *text, float *x, float *y) { + mbstate_t mbstate = {0}; + TextRenderState render_state = {*x, *y}; + text_chars_begin(font); + char32_t c = 0; + char const *end = text + strlen(text); + while (text != end) { + size_t ret = mbrtoc32(&c, text, (size_t)(end - text), &mbstate); + if (ret == 0) break; + if (ret == (size_t)(-2)) { // incomplete multi-byte character + text_render_char_internal(font, '?', &render_state); + text = end; // done reading text + } else if (ret == (size_t)(-1)) { + // invalid UTF-8; skip this byte + text_render_char_internal(font, '?', &render_state); + ++text; + } else { + if (ret != (size_t)(-3)) + text += ret; // character consists of `ret` bytes + switch (c) { + default: + text_render_char_internal(font, (char32_t)c, &render_state); + break; + } + } + } + text_chars_end(font); + *x = render_state.x; + *y = render_state.y; +} + void text_render(Font *font, char const *text, float x, float y) { - + text_render_internal(font, text, &x, &y); } void text_get_size(Font *font, char const *text, float *width, float *height) { + float x = 0, y = 0; + text_render_internal(font, text, &x, &y); + if (width) *width = x; + if (height) *height = y + font->char_height * (2/3.0f); +} + +void text_font_free(Font *font) { + free(font->ttf_data); + stbtt_bakedchar **char_pages = font->char_pages; + for (int i = 0; i < CHAR_PAGE_COUNT; ++i) { + if (char_pages[i]) { + free(char_pages[i]); + } + } + free(font); } -#endif @@ -1,11 +1,33 @@ #ifndef TEXT_H_ #define TEXT_H_ +#include <uchar.h> + +// A text-rendering interface. +// You can either use the simple API (text_render) +// or the character-by-character API (text_chars_begin, text_chars_end, text_render_char) + + typedef struct Font Font; -char const *text_get_err(void); +extern bool text_has_err(void); +// Get the current error. Errors will NOT be overwritten with newer errors. +extern char const *text_get_err(void); +// Clear the current error. +extern void text_clear_err(void); +// Load a TTF font found in ttf_filename with the given font size (character pixel height) extern Font *text_font_load(char const *ttf_filename, float font_size); +// Render some UTF-8 text to the screen (simple interface). extern void text_render(Font *font, char const *text, float x, float y); +// Get the dimensions of some text. extern void text_get_size(Font *font, char const *text, float *width, float *height); +// Begin writing characters. +extern void text_chars_begin(Font *font); +// Finish writing characters. +extern void text_chars_end(Font *font); +// Render a single character. +extern void text_render_char(Font *font, char32_t c, float *x, float *y); +// Free memory used by font. +extern void text_font_free(Font *font); #endif diff --git a/valgrind_suppresions.txt b/valgrind_suppresions.txt new file mode 100644 index 0000000..21b3680 --- /dev/null +++ b/valgrind_suppresions.txt @@ -0,0 +1,70 @@ +{ + Ignore dlopen . + Memcheck:Leak + ... + fun:_dl_open + ... +} +{ + Ignore dlopen . + Memcheck:Leak + ... + fun:_dlerror_run + ... +} +{ + Ignore dlopen . + Memcheck:Leak + ... + fun:_dl_init + ... +} +{ + Ignore. + Memcheck:Leak + ... + obj:*/radeonsi_dri.so + ... +} +{ + Ignore. + Memcheck:Leak + ... + fun:_XlcCreateLC + ... +} +{ + Ignore. + Memcheck:Leak + ... + obj:*LLVM* + ... +} +{ + Ignore. + Memcheck:Leak + ... + fun:dbus_connection_send_with_reply_and_block + ... +} +{ + Ignore. + Memcheck:Leak + ... + fun:__glDispatchMakeCurrent + ... +} +{ + Ignore. + Memcheck:Leak + ... + obj:*libdbus-1.so* + ... +} +{ + Ignore. + Memcheck:Leak + ... + fun:XSetLocaleModifiers + ... +} |