From 8c259a758b20a8cc62a04cbc360de0e1e1428382 Mon Sep 17 00:00:00 2001 From: pommicket Date: Tue, 18 Jul 2023 12:05:33 -0400 Subject: an initial draft of the new font rendering --- lib/stb_rect_pack.h | 623 ++++++++++++++++++++++++++++++++++++++++++++++++++++ stb_truetype.c | 9 +- text.c | 363 ++++++++++++++++-------------- 3 files changed, 835 insertions(+), 160 deletions(-) create mode 100644 lib/stb_rect_pack.h diff --git a/lib/stb_rect_pack.h b/lib/stb_rect_pack.h new file mode 100644 index 0000000..6a633ce --- /dev/null +++ b/lib/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 them. +// +// Rectangles which are successfully packed have the 'was_packed' flag +// set to a non-zero value and 'x' and 'y' store the minimum location +// on each axis (i.e. bottom-left in cartesian coordinates, top-left +// if you imagine y increasing downwards). Rectangles which do not fit +// have the 'was_packed' flag set to 0. +// +// You should not try to access the 'rects' array from another thread +// while this function is running, as the function temporarily reorders +// the array while it executes. +// +// To pack into another rectangle, you need to call stbrp_init_target +// again. To continue packing into the same rectangle, you can call +// this function again. Calling this multiple times with multiple rect +// arrays will probably produce worse packing results than calling it +// a single time with the full rectangle array, but the option is +// available. +// +// The function returns 1 if all of the rectangles were successfully +// packed and 0 otherwise. + +struct stbrp_rect +{ + // reserved for your use: + int id; + + // input: + stbrp_coord w, h; + + // output: + stbrp_coord x, y; + int was_packed; // non-zero if valid packing + +}; // 16 bytes, nominally + + +STBRP_DEF void stbrp_init_target (stbrp_context *context, int width, int height, stbrp_node *nodes, int num_nodes); +// Initialize a rectangle packer to: +// pack a rectangle that is 'width' by 'height' in dimensions +// using temporary storage provided by the array 'nodes', which is 'num_nodes' long +// +// You must call this function every time you start packing into a new target. +// +// There is no "shutdown" function. The 'nodes' memory must stay valid for +// the following stbrp_pack_rects() call (or calls), but can be freed after +// the call (or calls) finish. +// +// Note: to guarantee best results, either: +// 1. make sure 'num_nodes' >= 'width' +// or 2. call stbrp_allow_out_of_mem() defined below with 'allow_out_of_mem = 1' +// +// If you don't do either of the above things, widths will be quantized to multiples +// of small integers to guarantee the algorithm doesn't run out of temporary storage. +// +// If you do #2, then the non-quantized algorithm will be used, but the algorithm +// may run out of temporary storage and be unable to pack some rectangles. + +STBRP_DEF void stbrp_setup_allow_out_of_mem (stbrp_context *context, int allow_out_of_mem); +// Optionally call this function after init but before doing any packing to +// change the handling of the out-of-temp-memory scenario, described above. +// If you call init again, this will be reset to the default (false). + + +STBRP_DEF void stbrp_setup_heuristic (stbrp_context *context, int heuristic); +// Optionally select which packing heuristic the library should use. Different +// heuristics will produce better/worse results for different data sets. +// If you call init again, this will be reset to the default. + +enum +{ + STBRP_HEURISTIC_Skyline_default=0, + STBRP_HEURISTIC_Skyline_BL_sortHeight = STBRP_HEURISTIC_Skyline_default, + STBRP_HEURISTIC_Skyline_BF_sortHeight +}; + + +////////////////////////////////////////////////////////////////////////////// +// +// the details of the following structures don't matter to you, but they must +// be visible so you can handle the memory allocations for them + +struct stbrp_node +{ + stbrp_coord x,y; + stbrp_node *next; +}; + +struct stbrp_context +{ + int width; + int height; + int align; + int init_mode; + int heuristic; + int num_nodes; + stbrp_node *active_head; + stbrp_node *free_head; + stbrp_node extra[2]; // we allocate two extra nodes so optimal user-node-count is 'width' not 'width+2' +}; + +#ifdef __cplusplus +} +#endif + +#endif + +////////////////////////////////////////////////////////////////////////////// +// +// IMPLEMENTATION SECTION +// + +#ifdef STB_RECT_PACK_IMPLEMENTATION +#ifndef STBRP_SORT +#include +#define STBRP_SORT qsort +#endif + +#ifndef STBRP_ASSERT +#include +#define STBRP_ASSERT assert +#endif + +#ifdef _MSC_VER +#define STBRP__NOTUSED(v) (void)(v) +#define STBRP__CDECL __cdecl +#else +#define STBRP__NOTUSED(v) (void)sizeof(v) +#define STBRP__CDECL +#endif + +enum +{ + STBRP__INIT_skyline = 1 +}; + +STBRP_DEF void stbrp_setup_heuristic(stbrp_context *context, int heuristic) +{ + switch (context->init_mode) { + case STBRP__INIT_skyline: + STBRP_ASSERT(heuristic == STBRP_HEURISTIC_Skyline_BL_sortHeight || heuristic == STBRP_HEURISTIC_Skyline_BF_sortHeight); + context->heuristic = heuristic; + break; + default: + STBRP_ASSERT(0); + } +} + +STBRP_DEF void stbrp_setup_allow_out_of_mem(stbrp_context *context, int allow_out_of_mem) +{ + if (allow_out_of_mem) + // if it's ok to run out of memory, then don't bother aligning them; + // this gives better packing, but may fail due to OOM (even though + // the rectangles easily fit). @TODO a smarter approach would be to only + // quantize once we've hit OOM, then we could get rid of this parameter. + context->align = 1; + else { + // if it's not ok to run out of memory, then quantize the widths + // so that num_nodes is always enough nodes. + // + // I.e. num_nodes * align >= width + // align >= width / num_nodes + // align = ceil(width/num_nodes) + + context->align = (context->width + context->num_nodes-1) / context->num_nodes; + } +} + +STBRP_DEF void stbrp_init_target(stbrp_context *context, int width, int height, stbrp_node *nodes, int num_nodes) +{ + int i; + + for (i=0; i < num_nodes-1; ++i) + nodes[i].next = &nodes[i+1]; + nodes[i].next = NULL; + context->init_mode = STBRP__INIT_skyline; + context->heuristic = STBRP_HEURISTIC_Skyline_default; + context->free_head = &nodes[0]; + context->active_head = &context->extra[0]; + context->width = width; + context->height = height; + context->num_nodes = num_nodes; + stbrp_setup_allow_out_of_mem(context, 0); + + // node 0 is the full width, node 1 is the sentinel (lets us not store width explicitly) + context->extra[0].x = 0; + context->extra[0].y = 0; + context->extra[0].next = &context->extra[1]; + context->extra[1].x = (stbrp_coord) width; + context->extra[1].y = (1<<30); + context->extra[1].next = NULL; +} + +// find minimum y position if it starts at x1 +static int stbrp__skyline_find_min_y(stbrp_context *c, stbrp_node *first, int x0, int width, int *pwaste) +{ + stbrp_node *node = first; + int x1 = x0 + width; + int min_y, visited_width, waste_area; + + STBRP__NOTUSED(c); + + STBRP_ASSERT(first->x <= x0); + + #if 0 + // skip in case we're past the node + while (node->next->x <= x0) + ++node; + #else + STBRP_ASSERT(node->next->x > x0); // we ended up handling this in the caller for efficiency + #endif + + STBRP_ASSERT(node->x <= x0); + + min_y = 0; + waste_area = 0; + visited_width = 0; + while (node->x < x1) { + if (node->y > min_y) { + // raise min_y higher. + // we've accounted for all waste up to min_y, + // but we'll now add more waste for everything we've visted + waste_area += visited_width * (node->y - min_y); + min_y = node->y; + // the first time through, visited_width might be reduced + if (node->x < x0) + visited_width += node->next->x - x0; + else + visited_width += node->next->x - node->x; + } else { + // add waste area + int under_width = node->next->x - node->x; + if (under_width + visited_width > width) + under_width = width - visited_width; + waste_area += under_width * (min_y - node->y); + visited_width += under_width; + } + node = node->next; + } + + *pwaste = waste_area; + return min_y; +} + +typedef struct +{ + int x,y; + stbrp_node **prev_link; +} stbrp__findresult; + +static stbrp__findresult stbrp__skyline_find_best_pos(stbrp_context *c, int width, int height) +{ + int best_waste = (1<<30), best_x, best_y = (1 << 30); + stbrp__findresult fr; + stbrp_node **prev, *node, *tail, **best = NULL; + + // align to multiple of c->align + width = (width + c->align - 1); + width -= width % c->align; + STBRP_ASSERT(width % c->align == 0); + + // if it can't possibly fit, bail immediately + if (width > c->width || height > c->height) { + fr.prev_link = NULL; + fr.x = fr.y = 0; + return fr; + } + + node = c->active_head; + prev = &c->active_head; + while (node->x + width <= c->width) { + int y,waste; + y = stbrp__skyline_find_min_y(c, node, node->x, width, &waste); + if (c->heuristic == STBRP_HEURISTIC_Skyline_BL_sortHeight) { // actually just want to test BL + // bottom left + if (y < best_y) { + best_y = y; + best = prev; + } + } else { + // best-fit + if (y + height <= c->height) { + // can only use it if it first vertically + if (y < best_y || (y == best_y && waste < best_waste)) { + best_y = y; + best_waste = waste; + best = prev; + } + } + } + prev = &node->next; + node = node->next; + } + + best_x = (best == NULL) ? 0 : (*best)->x; + + // if doing best-fit (BF), we also have to try aligning right edge to each node position + // + // e.g, if fitting + // + // ____________________ + // |____________________| + // + // into + // + // | | + // | ____________| + // |____________| + // + // then right-aligned reduces waste, but bottom-left BL is always chooses left-aligned + // + // This makes BF take about 2x the time + + if (c->heuristic == STBRP_HEURISTIC_Skyline_BF_sortHeight) { + tail = c->active_head; + node = c->active_head; + prev = &c->active_head; + // find first node that's admissible + while (tail->x < width) + tail = tail->next; + while (tail) { + int xpos = tail->x - width; + int y,waste; + STBRP_ASSERT(xpos >= 0); + // find the left position that matches this + while (node->next->x <= xpos) { + prev = &node->next; + node = node->next; + } + STBRP_ASSERT(node->next->x > xpos && node->x <= xpos); + y = stbrp__skyline_find_min_y(c, node, xpos, width, &waste); + if (y + height <= c->height) { + if (y <= best_y) { + if (y < best_y || waste < best_waste || (waste==best_waste && xpos < best_x)) { + best_x = xpos; + STBRP_ASSERT(y <= best_y); + best_y = y; + best_waste = waste; + best = prev; + } + } + } + tail = tail->next; + } + } + + fr.prev_link = best; + fr.x = best_x; + fr.y = best_y; + return fr; +} + +static stbrp__findresult stbrp__skyline_pack_rectangle(stbrp_context *context, int width, int height) +{ + // find best position according to heuristic + stbrp__findresult res = stbrp__skyline_find_best_pos(context, width, height); + stbrp_node *node, *cur; + + // bail if: + // 1. it failed + // 2. the best node doesn't fit (we don't always check this) + // 3. we're out of memory + if (res.prev_link == NULL || res.y + height > context->height || context->free_head == NULL) { + res.prev_link = NULL; + return res; + } + + // on success, create new node + node = context->free_head; + node->x = (stbrp_coord) res.x; + node->y = (stbrp_coord) (res.y + height); + + context->free_head = node->next; + + // insert the new node into the right starting point, and + // let 'cur' point to the remaining nodes needing to be + // stiched back in + + cur = *res.prev_link; + if (cur->x < res.x) { + // preserve the existing one, so start testing with the next one + stbrp_node *next = cur->next; + cur->next = node; + cur = next; + } else { + *res.prev_link = node; + } + + // from here, traverse cur and free the nodes, until we get to one + // that shouldn't be freed + while (cur->next && cur->next->x <= res.x + width) { + stbrp_node *next = cur->next; + // move the current node to the free list + cur->next = context->free_head; + context->free_head = cur; + cur = next; + } + + // stitch the list back in + node->next = cur; + + if (cur->x < res.x + width) + cur->x = (stbrp_coord) (res.x + width); + +#ifdef _DEBUG + cur = context->active_head; + while (cur->x < context->width) { + STBRP_ASSERT(cur->x < cur->next->x); + cur = cur->next; + } + STBRP_ASSERT(cur->next == NULL); + + { + int count=0; + cur = context->active_head; + while (cur) { + cur = cur->next; + ++count; + } + cur = context->free_head; + while (cur) { + cur = cur->next; + ++count; + } + STBRP_ASSERT(count == context->num_nodes+2); + } +#endif + + return res; +} + +static int STBRP__CDECL rect_height_compare(const void *a, const void *b) +{ + const stbrp_rect *p = (const stbrp_rect *) a; + const stbrp_rect *q = (const stbrp_rect *) b; + if (p->h > q->h) + return -1; + if (p->h < q->h) + return 1; + return (p->w > q->w) ? -1 : (p->w < q->w); +} + +static int STBRP__CDECL rect_original_order(const void *a, const void *b) +{ + const stbrp_rect *p = (const stbrp_rect *) a; + const stbrp_rect *q = (const stbrp_rect *) b; + return (p->was_packed < q->was_packed) ? -1 : (p->was_packed > q->was_packed); +} + +STBRP_DEF int stbrp_pack_rects(stbrp_context *context, stbrp_rect *rects, int num_rects) +{ + int i, all_rects_packed = 1; + + // we use the 'was_packed' field internally to allow sorting/unsorting + for (i=0; i < num_rects; ++i) { + rects[i].was_packed = i; + } + + // sort according to heuristic + STBRP_SORT(rects, num_rects, sizeof(rects[0]), rect_height_compare); + + for (i=0; i < num_rects; ++i) { + if (rects[i].w == 0 || rects[i].h == 0) { + rects[i].x = rects[i].y = 0; // empty rect needs no space + } else { + stbrp__findresult fr = stbrp__skyline_pack_rectangle(context, rects[i].w, rects[i].h); + if (fr.prev_link) { + rects[i].x = (stbrp_coord) fr.x; + rects[i].y = (stbrp_coord) fr.y; + } else { + rects[i].x = rects[i].y = STBRP__MAXVAL; + } + } + } + + // unsort + STBRP_SORT(rects, num_rects, sizeof(rects[0]), rect_original_order); + + // set was_packed flags and all_rects_packed status + for (i=0; i < num_rects; ++i) { + rects[i].was_packed = !(rects[i].x == STBRP__MAXVAL && rects[i].y == STBRP__MAXVAL); + if (!rects[i].was_packed) + all_rects_packed = 0; + } + + // return the all_rects_packed status + return all_rects_packed; +} +#endif + +/* +------------------------------------------------------------------------------ +This software is available under 2 licenses -- choose whichever you prefer. +------------------------------------------------------------------------------ +ALTERNATIVE A - MIT License +Copyright (c) 2017 Sean Barrett +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +------------------------------------------------------------------------------ +ALTERNATIVE B - Public Domain (www.unlicense.org) +This is free and unencumbered software released into the public domain. +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, +commercial or non-commercial, and by any means. +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +------------------------------------------------------------------------------ +*/ diff --git a/stb_truetype.c b/stb_truetype.c index 2df7a90..07af6b9 100644 --- a/stb_truetype.c +++ b/stb_truetype.c @@ -1,4 +1,4 @@ -// used for debug build to speed things up +// used to speed things up for debug build // just exports everything in stb_truetype.h @@ -17,7 +17,14 @@ #define no_warn_end #endif +#if !DEBUG +#define STBTT_STATIC +#define STBRP_STATIC +#endif #define STB_TRUETYPE_IMPLEMENTATION +#define STB_RECT_PACK_IMPLEMENTATION + no_warn_start +#include "lib/stb_rect_pack.h" #include "lib/stb_truetype.h" no_warn_end diff --git a/text.c b/text.c index c869a5e..f2994aa 100644 --- a/text.c +++ b/text.c @@ -1,31 +1,18 @@ #include "ted.h" - + +no_warn_start #if DEBUG -typedef struct -{ - unsigned short x0,y0,x1,y1; // coordinates of bbox in bitmap - float xoff,yoff,xadvance; -} stbtt_bakedchar; -typedef struct -{ - float x0,y0,s0,t0; // top-left - float x1,y1,s1,t1; // bottom-right -} stbtt_aligned_quad; - -extern void stbtt_GetBakedQuad(const stbtt_bakedchar *chardata, int pw, int ph, int char_index, float *xpos, float *ypos, stbtt_aligned_quad *q, int opengl_fillrule); -extern int stbtt_BakeFontBitmap(const unsigned char *data, int offset, float pixel_height, unsigned char *pixels, int pw, int ph, int first_char, int num_chars, stbtt_bakedchar *chardata); +#include "lib/stb_rect_pack.h" +#include "lib/stb_truetype.h" #else -#define STBTT_STATIC -no_warn_start #include "stb_truetype.c" -no_warn_end #endif +no_warn_end - -// 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 +//no_warn_start +//#define STB_IMAGE_WRITE_IMPLEMENTATION +//#include "/~/apps/stb/stb_image_write.h" +//no_warn_end typedef struct { vec2 pos; @@ -37,16 +24,37 @@ typedef struct { TextVertex vert1, vert2, vert3; } TextTriangle; +typedef struct { + char32_t c; + u32 texture; + stbtt_packedchar data; +} CharInfo; + +// characters are split into this many "buckets" according to +// their least significant bits. this is to create a Budget Hash Map™. +// must be a power of 2. +#define CHAR_BUCKET_COUNT (1 << 10) + +#define FONT_TEXTURE_WIDTH 512 // width of each texture +#define FONT_TEXTURE_HEIGHT 512 // height of each texture + +typedef struct { + GLuint tex; + bool needs_update; + unsigned char *pixels; + TextTriangle *triangles; +} FontTexture; + struct Font { bool force_monospace; float space_width; // width of the character ' '. calculated when font is loaded. float char_height; - 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. + stbtt_pack_context pack_context; + stbtt_fontinfo stb_info; + FontTexture *textures; // dynamic array of textures + CharInfo *char_info[CHAR_BUCKET_COUNT]; // each entry is a dynamic array of char info // TTF data (i.e. the contents of the TTF file) u8 *ttf_data; - TextTriangle *triangles[CHAR_PAGE_COUNT]; // triangles to render for each page }; const TextRenderState text_render_state_default = { @@ -122,14 +130,49 @@ void main() {\n\ return true; } -static Status text_load_char_page(Font *font, int page) { - if (font->char_pages[page]) { - // already loaded - return true; +static u32 char_bucket_index(char32_t c) { + return c & (CHAR_BUCKET_COUNT - 1); +} + +// on success, *info is filled out +static Status text_load_char(Font *font, char32_t c, CharInfo *info) { + u32 bucket = char_bucket_index(c); + arr_foreach_ptr(font->char_info[bucket], CharInfo, i) { + if (i->c == c) { + // already loaded + *info = *i; + return true; + } } glGetError(); // clear error + if (!font->textures) { + unsigned char *pixels = calloc(FONT_TEXTURE_WIDTH, FONT_TEXTURE_HEIGHT); + if (!pixels) { + text_set_err("Not enough memory for font bitmap."); + return false; + } + stbtt_PackBegin(&font->pack_context, pixels, FONT_TEXTURE_WIDTH, FONT_TEXTURE_HEIGHT, + FONT_TEXTURE_WIDTH, 1, NULL); + FontTexture *texture = arr_addp(font->textures); + glGenTextures(1, &texture->tex); + texture->pixels = pixels; + } + + FontTexture *texture = arr_lastp(font->textures); + { + info->c = c; + info->texture = arr_len(font->textures) - 1; + int success = stbtt_PackFontRange(&font->pack_context, font->ttf_data, 0, font->char_height, + (int)c, 1, &info->data); + printf("%lc %d\n",c,success); + } + texture->needs_update = true; + + arr_add(font->char_info[bucket], *info); + return true; +#if 0 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); @@ -174,6 +217,8 @@ static Status text_load_char_page(Font *font, int page) { return false; } return true; +#endif + } Font *text_font_load(const char *ttf_filename, float font_size) { @@ -182,40 +227,43 @@ Font *text_font_load(const char *ttf_filename, float font_size) { text_clear_err(); - 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); - if (file_data && font) { - size_t bytes_read = fread(file_data, 1, file_size, ttf_file); - if (bytes_read == file_size) { - font->char_height = font_size; - font->ttf_data = file_data; - if (text_load_char_page(font, 0)) { // load page with Latin text, etc. - // calculate width of the character ' ' - font->space_width = font->char_pages[0][' '].xadvance; - } - } else { - text_set_err("Couldn't read font file."); - } - } else { - text_set_err("Not enough memory for font."); - } - if (text_has_err()) { - free(file_data); - free(font); - font = NULL; + if (!ttf_file) { + text_set_err("Couldn't open font file.", ttf_filename); + return NULL; + } + + 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 + text_set_err("Font file too big (%u megabytes).", (uint)(file_size >> 20)); + fclose(ttf_file); + return NULL; + } + u8 *file_data = calloc(1, file_size); + font = calloc(1, sizeof *font); + if (file_data && font) { + size_t bytes_read = fread(file_data, 1, file_size, ttf_file); + if (bytes_read == file_size) { + font->char_height = font_size; + font->ttf_data = file_data; + CharInfo space = {0}; + if (text_load_char(font, ' ', &space)) { + // calculate width of the character ' ' + font->space_width = space.data.xadvance; } - fclose(ttf_file); } else { - text_set_err("Font file too big (%u megabytes).", (uint)(file_size >> 20)); + text_set_err("Couldn't read font file."); } } else { - text_set_err("Couldn't open font file.", ttf_filename); + text_set_err("Not enough memory for font."); } + if (text_has_err()) { + free(file_data); + free(font); + font = NULL; + } + fclose(ttf_file); return font; } @@ -229,24 +277,31 @@ float text_font_char_height(Font *font) { float text_font_char_width(Font *font, char32_t c) { if (c == ' ') return font->space_width; - uint page = c / CHAR_PAGE_SIZE; - uint index = c % CHAR_PAGE_SIZE; - if (!font->char_pages[page]) - if (!text_load_char_page(font, (int)page)) - return font->space_width; - return font->char_pages[page][index].xadvance; + CharInfo info = {0}; + if (text_load_char(font, c, &info)) + return info.data.xadvance; + else + return 0; } void text_render(Font *font) { - for (uint i = 0; i < CHAR_PAGE_COUNT; ++i) { - if (font->triangles[i]) { + arr_foreach_ptr(font->textures, FontTexture, texture) { + size_t ntriangles = arr_len(texture->triangles); + if (ntriangles) { + if (texture->needs_update) { + glBindTexture(GL_TEXTURE_2D, texture->tex); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, FONT_TEXTURE_WIDTH, FONT_TEXTURE_HEIGHT, 0, GL_RED, GL_UNSIGNED_BYTE, texture->pixels); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + //stbi_write_png("out.png", FONT_TEXTURE_WIDTH, FONT_TEXTURE_HEIGHT, 1, texture->pixels, FONT_TEXTURE_WIDTH); + texture->needs_update = false; + } // render these triangles - size_t ntriangles = arr_len(font->triangles[i]); - if (gl_version_major >= 3) glBindVertexArray(text_vao); glBindBuffer(GL_ARRAY_BUFFER, text_vbo); - glBufferData(GL_ARRAY_BUFFER, (GLsizeiptr)(ntriangles * sizeof(TextTriangle)), font->triangles[i], GL_STREAM_DRAW); + glBufferData(GL_ARRAY_BUFFER, (GLsizeiptr)(ntriangles * sizeof(TextTriangle)), texture->triangles, GL_STREAM_DRAW); glVertexAttribPointer(text_v_pos, 2, GL_FLOAT, 0, sizeof(TextVertex), (void *)offsetof(TextVertex, pos)); glEnableVertexAttribArray(text_v_pos); glVertexAttribPointer(text_v_tex_coord, 2, GL_FLOAT, 0, sizeof(TextVertex), (void *)offsetof(TextVertex, tex_coord)); @@ -255,11 +310,11 @@ void text_render(Font *font) { glEnableVertexAttribArray(text_v_color); glUseProgram(text_program); glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, font->textures[i]); + glBindTexture(GL_TEXTURE_2D, texture->tex); glUniform1i(text_u_sampler, 0); glUniform2f(text_u_window_size, gl_window_width, gl_window_height); glDrawArrays(GL_TRIANGLES, 0, (GLsizei)(3 * ntriangles)); - arr_clear(font->triangles[i]); + arr_clear(texture->triangles); } } } @@ -274,94 +329,86 @@ top: } if (c >= UNICODE_CODE_POINTS) c = UNICODE_BOX_CHARACTER; // code points this big should never appear in valid Unicode + CharInfo info = {0}; + + if (!text_load_char(font, c, &info)) + return; + const float char_height = font->char_height; - uint page = c / CHAR_PAGE_SIZE; - uint index = c % CHAR_PAGE_SIZE; - if (state->render) { - if (!font->char_pages[page]) - if (!text_load_char_page(font, (int)page)) - return; + stbtt_aligned_quad q = {0}; + + if (state->wrap && c == '\n') { + state->x = state->min_x; + state->y += char_height; + goto ret; } - stbtt_bakedchar *char_data = font->char_pages[page]; - const float char_height = font->char_height; - if (char_data) { // if page was successfully loaded - stbtt_aligned_quad q = {0}; - - if (state->wrap && c == '\n') { - state->x = state->min_x; - state->y += char_height; - goto ret; - } + { + float x, y; + x = (float)(state->x - floor(state->x)); + y = (float)(state->y - floor(state->y)); + y += char_height * 0.75f; + stbtt_GetPackedQuad(&info.data, FONT_TEXTURE_WIDTH, FONT_TEXTURE_HEIGHT, 0, &x, &y, &q, 1); + y -= char_height * 0.75f; - { - float x, y; - x = (float)(state->x - floor(state->x)); - y = (float)(state->y - floor(state->y)); - y += char_height * 0.75f; - stbtt_GetBakedQuad(char_data, font->tex_widths[page], font->tex_heights[page], - (int)index, &x, &y, &q, 1); - y -= char_height * 0.75f; - - q.x0 += (float)floor(state->x); - q.y0 += (float)floor(state->y); - q.x1 += (float)floor(state->x); - q.y1 += (float)floor(state->y); - - if (font->force_monospace) { - state->x += font->space_width; // ignore actual character width - } else { - state->x = x + floor(state->x); - state->y = y + floor(state->y); - } - } + q.x0 += (float)floor(state->x); + q.y0 += (float)floor(state->y); + q.x1 += (float)floor(state->x); + q.y1 += (float)floor(state->y); - float s0 = q.s0, t0 = q.t0; - float s1 = q.s1, t1 = q.t1; - float x0 = q.x0, y0 = q.y0; - float x1 = q.x1, y1 = q.y1; - const float min_x = state->min_x, max_x = state->max_x; - const float min_y = state->min_y, max_y = state->max_y; - - if (state->wrap && x1 >= max_x) { - state->x = min_x; - state->y += char_height; - goto top; + if (font->force_monospace) { + state->x += font->space_width; // ignore actual character width + } else { + state->x = x + floor(state->x); + state->y = y + floor(state->y); } + } + + float s0 = q.s0, t0 = q.t0; + float s1 = q.s1, t1 = q.t1; + float x0 = q.x0, y0 = q.y0; + float x1 = q.x1, y1 = q.y1; + const float min_x = state->min_x, max_x = state->max_x; + const float min_y = state->min_y, max_y = state->max_y; + + if (state->wrap && x1 >= max_x) { + state->x = min_x; + state->y += char_height; + goto top; + } - if (x0 > max_x || y0 > max_y || x1 < min_x || y1 < min_y) - goto ret; - if (x0 < min_x) { - // left side of character is clipped - s0 = (min_x-x0) / (x1-x0) * (s1-s0) + s0; - x0 = min_x; - } - if (x1 >= max_x) { - // right side of character is clipped - s1 = (max_x-1-x0) / (x1-x0) * (s1-s0) + s0; - x1 = max_x-1; - } - if (y0 < min_y) { - // top side of character is clipped - t0 = (min_y-y0) / (y1-y0) * (t1-t0) + t0; - y0 = min_y; - } - if (y1 >= max_y) { - // bottom side of character is clipped - t1 = (max_y-1-y0) / (y1-y0) * (t1-t0) + t0; - y1 = max_y-1; - } - if (state->render) { - float r = state->color[0], g = state->color[1], b = state->color[2], a = state->color[3]; - TextVertex v_1 = {{x0, y0}, {s0, t0}, {r, g, b, a}}; - TextVertex v_2 = {{x0, y1}, {s0, t1}, {r, g, b, a}}; - TextVertex v_3 = {{x1, y1}, {s1, t1}, {r, g, b, a}}; - TextVertex v_4 = {{x1, y0}, {s1, t0}, {r, g, b, a}}; - TextTriangle triangle1 = {v_1, v_2, v_3}; - TextTriangle triangle2 = {v_3, v_4, v_1}; - arr_add(font->triangles[page], triangle1); - arr_add(font->triangles[page], triangle2); - } + if (x0 > max_x || y0 > max_y || x1 < min_x || y1 < min_y) + goto ret; + if (x0 < min_x) { + // left side of character is clipped + s0 = (min_x-x0) / (x1-x0) * (s1-s0) + s0; + x0 = min_x; + } + if (x1 >= max_x) { + // right side of character is clipped + s1 = (max_x-1-x0) / (x1-x0) * (s1-s0) + s0; + x1 = max_x-1; + } + if (y0 < min_y) { + // top side of character is clipped + t0 = (min_y-y0) / (y1-y0) * (t1-t0) + t0; + y0 = min_y; + } + if (y1 >= max_y) { + // bottom side of character is clipped + t1 = (max_y-1-y0) / (y1-y0) * (t1-t0) + t0; + y1 = max_y-1; + } + if (state->render) { + float r = state->color[0], g = state->color[1], b = state->color[2], a = state->color[3]; + TextVertex v_1 = {{x0, y0}, {s0, t0}, {r, g, b, a}}; + TextVertex v_2 = {{x0, y1}, {s0, t1}, {r, g, b, a}}; + TextVertex v_3 = {{x1, y1}, {s1, t1}, {r, g, b, a}}; + TextVertex v_4 = {{x1, y0}, {s1, t0}, {r, g, b, a}}; + TextTriangle triangle1 = {v_1, v_2, v_3}; + TextTriangle triangle2 = {v_3, v_4, v_1}; + arr_add(font->textures[info.texture].triangles, triangle1); + arr_add(font->textures[info.texture].triangles, triangle2); } ret: if (state->x > state->x_largest) @@ -449,12 +496,10 @@ void text_get_size32(Font *font, const char32_t *text, u64 len, float *width, fl 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]); - } - arr_clear(font->triangles[i]); + arr_foreach_ptr(font->textures, FontTexture, texture) { + glDeleteTextures(1, &texture->tex); + arr_free(texture->triangles); } + arr_free(font->textures); free(font); } -- cgit v1.2.3