taisei/src/resource/font.c

487 lines
11 KiB
C

/*
* This software is licensed under the terms of the MIT-License
* See COPYING for further information.
* ---
* Copyright (c) 2011-2018, Lukas Weber <laochailan@web.de>.
* Copyright (c) 2012-2018, Andrei Alexeyev <akari@alienslab.net>.
*/
#include "taisei.h"
#include "font.h"
#include "global.h"
#include "util.h"
#include "objectpool.h"
#include "objectpool_util.h"
#define CACHE_EXPIRE_TIME 1000
#ifdef DEBUG
// #define VERBOSE_CACHE_LOG
#endif
#ifdef VERBOSE_CACHE_LOG
#define CACHELOG(fmt, ...) log_debug(fmt, __VA_ARGS__)
#else
#define CACHELOG(fmt, ...)
#endif
typedef struct CacheEntry {
OBJECT_INTERFACE(struct CacheEntry);
SDL_Surface *surf;
int width;
int height;
uint32_t ref_time;
struct {
// to simplify invalidation
Hashtable *ht;
char *ht_key;
} owner;
} CacheEntry;
typedef struct FontRenderer {
Texture tex;
float quality;
uint32_t *pixbuf;
} FontRenderer;
static ObjectPool *cache_pool;
static CacheEntry *cache_entries;
static FontRenderer font_renderer;
struct Font {
TTF_Font *ttf;
Hashtable *cache;
};
struct Fonts _fonts;
static TTF_Font* load_ttf(char *vfspath, int size) {
char *syspath = vfs_repr(vfspath, true);
SDL_RWops *rwops = vfs_open(vfspath, VFS_MODE_READ | VFS_MODE_SEEKABLE);
if(!rwops) {
log_fatal("VFS error: %s", vfs_get_error());
}
// XXX: what would be the best rounding strategy here?
size = rint(size * font_renderer.quality);
TTF_Font *f = TTF_OpenFontRW(rwops, true, size);
if(!f) {
log_fatal("Failed to load font '%s' @ %i: %s", syspath, size, TTF_GetError());
}
log_info("Loaded '%s' @ %i", syspath, size);
free(syspath);
return f;
}
static Font* load_font(char *vfspath, int size) {
TTF_Font *ttf = load_ttf(vfspath, size);
Font *font = calloc(1, sizeof(Font));
font->ttf = ttf;
font->cache = hashtable_new_stringkeys(2048);
return font;
}
static void free_cache_entry(CacheEntry *e) {
if(!e) {
return;
}
if(e->surf) {
SDL_FreeSurface(e->surf);
}
CACHELOG("Wiping cache entry %p [%s]", (void*)e, e->owner.ht_key);
free(e->owner.ht_key);
list_unlink(&cache_entries, e);
objpool_release(cache_pool, (ObjectInterface*)e);
}
static CacheEntry* get_cache_entry(Font *font, const char *text) {
CacheEntry *e = hashtable_get_unsafe(font->cache, (void*)text);
if(!e) {
if(objpool_is_full(cache_pool)) {
CacheEntry *oldest = cache_entries;
for(CacheEntry *e = cache_entries->next; e; e = e->next) {
if(e->ref_time < oldest->ref_time) {
oldest = e;
}
}
hashtable_unset_string(oldest->owner.ht, oldest->owner.ht_key);
free_cache_entry(oldest);
}
e = (CacheEntry*)objpool_acquire(cache_pool);
list_push(&cache_entries, e);
hashtable_set_string(font->cache, text, e);
e->owner.ht = font->cache;
e->owner.ht_key = strdup(text);
CACHELOG("New entry for text: [%s]", text);
}
e->ref_time = SDL_GetTicks();
return e;
}
void update_font_cache(void) {
uint32_t now = SDL_GetTicks();
CacheEntry *next;
for(CacheEntry *e = cache_entries; e; e = next) {
next = e->next;
if(now - e->ref_time > CACHE_EXPIRE_TIME) {
hashtable_unset(e->owner.ht, e->owner.ht_key);
free_cache_entry(e);
}
}
}
static void fontrenderer_init(FontRenderer *f, float quality) {
f->quality = quality = sanitize_scale(quality);
float r = ftopow2(quality);
int w = FONTREN_MAXW * r;
int h = FONTREN_MAXH * r;
glGenTextures(1,&f->tex.gltex);
f->tex.truew = w;
f->tex.trueh = h;
f->pixbuf = calloc(f->tex.truew, f->tex.trueh);
glBindTexture(GL_TEXTURE_2D,f->tex.gltex);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, f->tex.truew, f->tex.trueh, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0);
log_debug("q=%f, w=%i, h=%i", f->quality, f->tex.truew, f->tex.trueh);
}
static void fontrenderer_free(FontRenderer *f) {
glDeleteTextures(1, &f->tex.gltex);
free(f->pixbuf);
}
static void fontrenderer_draw_prerendered(FontRenderer *f, SDL_Surface *surf) {
assert(surf != NULL);
glBindTexture(GL_TEXTURE_2D, f->tex.gltex);
f->tex.w = surf->w;
f->tex.h = surf->h;
// the written texture is zero padded to avoid bits of previously drawn text bleeding in
int winw = surf->w+1;
int winh = surf->h+1;
uint32_t *pixels = f->pixbuf;
for(int y = 0; y < surf->h; y++) {
memcpy(pixels+y*winw, ((uint8_t *)surf->pixels)+y*surf->pitch, surf->w*4);
pixels[y*winw+surf->w]=0;
}
memset(pixels+(winh-1)*winw,0,winw*4);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, winw, winh, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
}
static SDL_Surface* fontrender_render(FontRenderer *f, const char *text, Font *font) {
CacheEntry *e = get_cache_entry(font, text);
SDL_Surface *surf = e->surf;
if(surf) {
return surf;
}
CACHELOG("Rendering text: [%s]", text);
surf = e->surf = TTF_RenderUTF8_Blended(font->ttf, text, (SDL_Color){255, 255, 255});
if(!surf) {
log_fatal("TTF_RenderUTF8_Blended() failed: %s", TTF_GetError());
}
if(surf->w > f->tex.truew || surf->h > f->tex.trueh) {
log_fatal("Text (%s %dx%d) is too big for the internal buffer (%dx%d).", text, surf->w, surf->h, f->tex.truew, f->tex.trueh);
}
return surf;
}
static void fontrenderer_draw(FontRenderer *f, const char *text, Font *font) {
SDL_Surface *surf = fontrender_render(f, text, font);
fontrenderer_draw_prerendered(f, surf);
}
void init_fonts(void) {
TTF_Init();
memset(&font_renderer, 0, sizeof(font_renderer));
cache_pool = OBJPOOL_ALLOC(CacheEntry, 512);
}
void uninit_fonts(void) {
free_fonts();
TTF_Quit();
}
void load_fonts(float quality) {
fontrenderer_init(&font_renderer, quality);
_fonts.standard = load_font("res/fonts/LinBiolinum.ttf", 20);
_fonts.mainmenu = load_font("res/fonts/immortal.ttf", 35);
_fonts.small = load_font("res/fonts/LinBiolinum.ttf", 14);
_fonts.hud = load_font("res/fonts/Laconic_Regular.otf", 19);
_fonts.mono = load_font("res/fonts/ShareTechMono-Regular.ttf", 19);
_fonts.monosmall = load_font("res/fonts/ShareTechMono-Regular.ttf", 14);
_fonts.monotiny = load_font("res/fonts/ShareTechMono-Regular.ttf", 10);
}
void reload_fonts(float quality) {
if(!font_renderer.quality) {
// never loaded
load_fonts(quality);
return;
}
if(font_renderer.quality != sanitize_scale(quality)) {
free_fonts();
load_fonts(quality);
}
}
static void free_font(Font *font) {
CacheEntry *e;
TTF_CloseFont(font->ttf);
for(HashtableIterator *i = hashtable_iter(font->cache); hashtable_iter_next(i, 0, (void**)&e);) {
free_cache_entry(e);
}
hashtable_free(font->cache);
free(font);
}
void free_fonts(void) {
fontrenderer_free(&font_renderer);
Font **last = &_fonts.first + (sizeof(_fonts)/sizeof(Font*) - 1);
for(Font **font = &_fonts.first; font <= last; ++font) {
free_font(*font);
}
}
static void draw_text_texture(Alignment align, float x, float y, Texture *tex) {
float m = 1.0 / font_renderer.quality;
bool adjust = !(align & AL_Flag_NoAdjust);
align &= 0xf;
if(adjust) {
switch(align) {
case AL_Center:
break;
// tex->w/2 is integer division and must be done first
case AL_Left:
x += m*(tex->w/2);
break;
case AL_Right:
x -= m*(tex->w/2);
break;
default:
log_fatal("Invalid alignment %x", align);
}
// if textures are odd pixeled, align them for ideal sharpness.
if(tex->w&1) {
x += 0.5;
}
if(tex->h&1) {
y += 0.5;
}
} else {
switch(align) {
case AL_Center:
break;
case AL_Left:
x += m*(tex->w/2.0);
break;
case AL_Right:
x -= m*(tex->w/2.0);
break;
default:
log_fatal("Invalid alignment %x", align);
}
}
glPushMatrix();
glTranslatef(x, y, 0);
glScalef(m, m, 1);
draw_texture_p(0, 0, tex);
glPopMatrix();
}
void draw_text(Alignment align, float x, float y, const char *text, Font *font) {
assert(text != NULL);
if(!*text) {
return;
}
char *nl;
char *buf = malloc(strlen(text)+1);
strcpy(buf, text);
if((nl = strchr(buf, '\n')) != NULL && strlen(nl) > 1) {
draw_text(align, x, y + 20, nl+1, font);
*nl = '\0';
}
fontrenderer_draw(&font_renderer, buf, font);
draw_text_texture(align, x, y, &font_renderer.tex);
free(buf);
}
Texture* render_text(const char *text, Font *font) {
fontrenderer_draw(&font_renderer, text, font);
return &font_renderer.tex;
}
void draw_text_auto_wrapped(Alignment align, float x, float y, const char *text, int width, Font *font) {
char buf[strlen(text) * 2];
wrap_text(buf, sizeof(buf), text, width, font);
draw_text(align, x, y, buf, font);
}
static void string_dimensions(char *s, Font *font, int *w, int *h) {
CacheEntry *e = get_cache_entry(font, s);
if(e->width <= 0 || e->height <= 0) {
TTF_SizeUTF8(font->ttf, s, &e->width, &e->height);
CACHELOG("Got size %ix%i for text: [%s]", e->width, e->height, s);
}
if(w) {
*w = e->width;
}
if(h) {
*h = e->height;
}
}
int stringwidth(char *s, Font *font) {
int w;
string_dimensions(s, font, &w, NULL);
return w / font_renderer.quality;
}
int stringheight(char *s, Font *font) {
int h;
string_dimensions(s, font, NULL, &h);
return h / font_renderer.quality;
}
int charwidth(char c, Font *font) {
char s[2];
s[0] = c;
s[1] = 0;
return stringwidth(s, font);
}
int font_line_spacing(Font *font) {
return TTF_FontLineSkip(font->ttf) / font_renderer.quality;
}
void shorten_text_up_to_width(char *s, float width, Font *font) {
while(stringwidth(s, font) > width) {
int l = strlen(s);
if(l <= 1) {
return;
}
--l;
s[l] = 0;
for(int i = 0; i < min(3, l); ++i) {
s[l - i - 1] = '.';
}
}
}
void wrap_text(char *buf, size_t bufsize, const char *src, int width, Font *font) {
assert(buf != NULL);
assert(src != NULL);
assert(font != NULL);
assert(bufsize > strlen(src) + 1);
assert(width > 0);
char src_copy[strlen(src) + 1];
char *sptr = src_copy;
char *next = NULL;
char *curline = buf;
strcpy(src_copy, src);
*buf = 0;
while((next = strtok_r(NULL, " \t\n", &sptr))) {
int curwidth;
if(!*next) {
continue;
}
if(*curline) {
curwidth = stringwidth(curline, font);
} else {
curwidth = 0;
}
char tmpbuf[strlen(curline) + strlen(next) + 2];
strcpy(tmpbuf, curline);
strcat(tmpbuf, " ");
strcat(tmpbuf, next);
int totalwidth = stringwidth(tmpbuf, font);
if(totalwidth > width) {
if(curwidth == 0) {
log_fatal(
"Single word '%s' won't fit on one line. "
"Word width: %i, max width: %i, source string: %s",
next, stringwidth(next, font), width, src
);
}
strlcat(buf, "\n", bufsize);
curline = strchr(curline, 0);
} else {
if(*curline) {
strlcat(buf, " ", bufsize);
}
}
strlcat(buf, next, bufsize);
}
}