Text rendering rewrite and optimizations; some refactoring (#129)

* wip font rendering stuff; hashtable monstrosity is temporary

* various text rendering fixes/improvements

* HashTables™ 3.0

* Add some comments to aid navigating the hashtable macro maze

* overhaul text rendering API; add default and example shaders

* text: implement text_render for spellcard effect; misc fixes

* README: update dependencies

Bye SDL_ttf, hello freetype2.

* text_draw: fix resolution/scale-dependent bugs

* make text_draw fallback to the current shader, fix hud and stagetext

* repair the bgm loading

* fix spell practice mode

* fix walloftext

forgot one site of text_draw earlier

* fix wrapped text rendering

* fix and simplify the hud text shader

* dynamic glyph cache

* implement font size change on window resize/quality setting change/etc.

* rename text shaders for consistency

* preloads for fonts and text shaders

* make the stagetext shader look somewhat better

* text_render: attempt to normalize height

* small improvement for stagetext
This commit is contained in:
Andrei Alexeyev 2018-06-30 00:36:51 +03:00 committed by GitHub
parent 433b9869e2
commit 322edd0dce
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
75 changed files with 3620 additions and 1429 deletions

View file

@ -16,11 +16,12 @@ Installation
Dependencies
^^^^^^^^^^^^
- SDL2 >= 2.0.5, SDL2_ttf, SDL2_mixer, SDL2_image
- SDL2 >= 2.0.5, SDL2_mixer, SDL2_image
- zlib
- libzip >= 1.0
- libpng >= 1.5.0
- libjpeg
- freetype2
- OpenGL >= 3.3
Build-only dependencies

View file

@ -94,6 +94,7 @@ foreach flag : [
'-Wunreachable-code-loop-increment',
'-Wgnu',
'-Wcast-align',
'-Wcast-align=strict',
]
if cc.has_argument(flag)
taisei_c_warnargs += flag
@ -105,21 +106,21 @@ taisei_c_args += taisei_c_warnargs
static = get_option('static')
dep_sdl2 = dependency('sdl2', version : '>=2.0.5', required : true, static : static)
dep_sdl2_ttf = dependency('SDL2_ttf', required : true, static : static)
dep_sdl2_mixer = dependency('SDL2_mixer', required : false, static : static)
dep_sdl2_image = dependency('SDL2_image', required : true, static : static)
dep_zlib = dependency('zlib', required : true, static : static)
dep_png = dependency('libpng', version : '>=1.5', required : true, static : static)
dep_zip = dependency('libzip', version : '>=1.0', required : false, static : static)
dep_freetype = dependency('freetype2', required : true, static : static)
dep_cglm = subproject('cglm').get_variable('cglm_dep')
dep_m = cc.find_library('m', required : false)
taisei_deps = [
dep_sdl2,
dep_sdl2_ttf,
dep_sdl2_image,
dep_zlib,
dep_png,
dep_freetype,
dep_m,
dep_cglm,
]

View file

@ -1,41 +0,0 @@
#version 330 core
#include "interface/standard.glslh"
UNIFORM(1) vec4 colorAtop;
UNIFORM(2) vec4 colorAbot;
UNIFORM(3) vec4 colorBtop;
UNIFORM(4) vec4 colorBbot;
UNIFORM(5) vec4 colortint;
UNIFORM(6) float split;
void main(void) {
vec4 texel = texture(tex, texCoord);
float vsplit;
vec4 cAt;
vec4 cAb;
vec4 cBt;
vec4 cBb;
// branching on a uniform is fine
if(split < 0.0) {
vsplit = -split;
cAt = colorBtop;
cAb = colorBbot;
cBt = colorAtop;
cBb = colorAbot;
} else {
vsplit = split;
cAt = colorAtop;
cAb = colorAbot;
cBt = colorBtop;
cBb = colorBbot;
}
fragColor = texel * colortint * (
texCoord.x >= vsplit ?
mix(cBt, cBb, texCoord.y) :
mix(cAt, cAb, texCoord.y)
);
}

View file

@ -1,2 +0,0 @@
glsl_objects = standard.vert hud_text.frag

View file

@ -15,13 +15,18 @@ ATTRIBUTE(2) vec2 vertTexCoord;
/*
* Per-instance attributes
*/
ATTRIBUTE(3) mat4 spriteVMTransform;
ATTRIBUTE(3) mat4 spriteVMTransform;
// 4
// 5
// 6
ATTRIBUTE(7) vec4 spriteRGBA;
ATTRIBUTE(8) vec4 spriteTexRegion;
ATTRIBUTE(9) float spriteCustomParam;
ATTRIBUTE(7) mat4 spriteTexTransform;
// 8
// 9
// 10
ATTRIBUTE(11) vec4 spriteRGBA;
ATTRIBUTE(12) vec4 spriteTexRegion;
ATTRIBUTE(13) vec2 spriteDimensions;
ATTRIBUTE(14) float spriteCustomParam;
#endif
#ifdef FRAG_STAGE
@ -30,10 +35,12 @@ OUT(0) vec4 fragColor;
UNIFORM(0) sampler2D tex;
VARYING(0) vec2 texCoordRaw;
VARYING(1) vec2 texCoord;
VARYING(2) vec4 texRegion;
VARYING(3) vec4 color;
VARYING(4) float customParam;
VARYING(0) vec2 texCoordRaw;
VARYING(1) vec2 texCoord;
VARYING(2) vec2 texCoordOverlay;
VARYING(3) vec4 texRegion;
VARYING(4) vec4 color;
VARYING(5) vec2 dimensions;
VARYING(6) float customParam;
#endif

View file

@ -19,10 +19,18 @@ void main(void) {
texCoord = uv_to_region(spriteTexRegion, vertTexCoord);
#endif
#ifdef SPRITE_OUT_TEXCOORD_OVERLAY
texCoordOverlay = (spriteTexTransform * vec4(vertTexCoord, 0.0, 1.0)).xy;
#endif
#ifdef SPRITE_OUT_TEXREGION
texRegion = spriteTexRegion;
#endif
#ifdef SPRITE_OUT_DIMENSIONS
dimensions = spriteDimensions;
#endif
#ifdef SPRITE_OUT_CUSTOM
customParam = spriteCustomParam;
#endif

View file

@ -13,7 +13,6 @@ glsl_files = files(
'boss_zoom.frag.glsl',
'glitch.frag.glsl',
'graph.frag.glsl',
'hud_text.frag.glsl',
'ingame_menu.frag.glsl',
'laser_generic.vert.glsl',
'marisa_laser.frag.glsl',
@ -39,11 +38,16 @@ glsl_files = files(
'sprite_youmu_myon_shot.frag.glsl',
'stage6_sky.frag.glsl',
'stage6_sky.vert.glsl',
'stagetext.frag.glsl',
'standard.frag.glsl',
'standard.vert.glsl',
'standardnotex.frag.glsl',
'standardnotex.vert.glsl',
'text_default.frag.glsl',
'text_default.vert.glsl',
'text_example.frag.glsl',
'text_example.vert.glsl',
'text_hud.frag.glsl',
'text_stagetext.frag.glsl',
'texture_post_load.frag.glsl',
'tower_light.frag.glsl',
'tower_light.vert.glsl',

View file

@ -13,6 +13,7 @@ void main(void) {
pos = mod(pos,vec2(1.0+(0.01/w),1.0));
pos *= vec2(w,h);
vec4 clr = texture(tex, pos);
clr.gb = clr.rr;
clr.a *= float(pos.x < w && pos.y < h)*(2.*t-t*t);
fragColor = clr;

View file

@ -1,21 +0,0 @@
#version 330 core
#include "lib/render_context.glslh"
#include "interface/standard.glslh"
UNIFORM(1) sampler2D trans;
UNIFORM(2) vec3 color;
UNIFORM(3) float t;
void main(void) {
vec2 pos = texCoordRaw;
vec2 f = pos-vec2(0.5,0.5);
pos += (f*0.05*sin(10.0*(length(f)+t)))*(1.0-t);
pos = clamp(pos,0.0,1.0);
vec4 texel = texture(tex, (r_textureMatrix*vec4(pos,0.0,1.0)).xy);
texel.a *= clamp((texture(trans, pos).r+0.5)*2.5*t-0.5, 0.0, 1.0);
fragColor = vec4(color,1.0)*texel;
}

View file

@ -1,2 +0,0 @@
glsl_objects = standard.vert stagetext.frag

View file

@ -0,0 +1,7 @@
#version 330 core
#include "interface/sprite.glslh"
void main(void) {
fragColor = color * vec4(1, 1, 1, texture(tex, texCoord).r);
}

View file

@ -0,0 +1 @@
glsl_objects = text_default.vert text_default.frag

View file

@ -0,0 +1,7 @@
#version 330 core
#define SPRITE_OUT_COLOR
#define SPRITE_OUT_TEXCOORD
#define SPRITE_OUT_TEXCOORD_OVERLAY
#include "lib/sprite_default.vert.glslh"

View file

@ -0,0 +1,30 @@
#version 330 core
#include "lib/render_context.glslh"
#include "interface/sprite.glslh"
#include "lib/util.glslh"
void main(void) {
vec2 tc = texCoord;
vec2 tc_overlay = texCoordOverlay;
// Transform local glyph texture coordinates.
tc *= dimensions;
tc.x += 5 * sin(3 * tc_overlay.y + customParam);
tc /= dimensions;
// Compute alpha multiplier used to chop off unwanted texels from the atlas.
float a = float(tc.x >= 0 && tc.x <= 1 && tc.y >= 0 && tc.y <= 1);
// Map local coordinates to the region of the atlas texture that contains our glyph.
vec2 tc_atlas = uv_to_region(texRegion, tc);
// Display the glyph.
fragColor = color * vec4(1, 1, 1, texture(tex, tc_atlas).r * a);
// Visualize global overlay coordinates. You could use them to span a texture across all glyphs.
fragColor *= vec4(tc_overlay.x, tc_overlay.y, 0, 1);
// Visualize the local coordinates.
// fragColor = vec4(mod(tc.x, 1), float(length(tc - vec2(0.5, 0.5)) < 0.5), mod(tc.y, 1), 1.0);
}

View file

@ -0,0 +1 @@
glsl_objects = text_example.vert text_example.frag

View file

@ -0,0 +1,35 @@
#version 330 core
#include "lib/defs.glslh"
#include "lib/render_context.glslh"
#include "lib/util.glslh"
#include "interface/sprite.glslh"
void main(void) {
// Enlarge the quad to make some room for effects.
float scale = 2;
vec2 pos = vertPos * scale;
gl_Position = r_projectionMatrix * spriteVMTransform * vec4(pos, 0.0, 1.0);
// Adjust texture coordinates so that the glyph remains in the center, unaffected by the scaling factor.
// Some extra code is required in the fragment shader to chop off the unwanted bits of the texture.
vec2 tc = vertTexCoord * scale - vec2(0.5 * (scale - 1));
// Pass tc not mapped to the texture region, because we want to do per-fragment transformations on it.
texCoord = tc;
// Pass the normalized texture region, so that we can map texCoord to it later in the fragment shader.
texRegion = spriteTexRegion;
// Global overlay coordinates for this primitive.
texCoordOverlay = (spriteTexTransform * vec4(tc, 0.0, 1.0)).xy;
// Fragment shader needs to know the sprite dimensions so that it can denormalize texCoord for processing.
dimensions = spriteDimensions;
// Arbitrary custom parameter provided by the application. You can use it to pass e.g. times/frames.
customParam = spriteCustomParam;
// Should be obvious.
color = spriteRGBA;
}

View file

@ -0,0 +1,9 @@
#version 330 core
#include "lib/render_context.glslh"
#include "interface/sprite.glslh"
void main(void) {
vec4 texel = texture(tex, texCoord);
fragColor = vec4(color.rgb, color.a * texel.r) * mix(1.0, 0.8, texCoordOverlay.y);
}

View file

@ -0,0 +1,2 @@
glsl_objects = text_default.vert text_hud.frag

View file

@ -0,0 +1,39 @@
#version 330 core
#include "lib/render_context.glslh"
#include "interface/sprite.glslh"
#include "lib/util.glslh"
UNIFORM(1) sampler2D trans;
float tc_mask(vec2 tc) {
return float(tc.x >= 0 && tc.x <= 1 && tc.y >= 0 && tc.y <= 1);
}
void main(void) {
float t = customParam;
vec2 tc = texCoord;
vec2 tc_overlay = texCoordOverlay;
vec2 f = tc_overlay - vec2(0.5);
tc *= dimensions;
tc += 5 * f * sin(10 * (length(f) + t)) * (1 - t);
tc /= dimensions;
float a = tc_mask(tc);
vec4 texel = texture(tex, uv_to_region(texRegion, tc));
vec4 textfrag = vec4(color.rgb, a * color.a * texel.r);
tc -= vec2(1) / dimensions;
a = tc_mask(tc);
texel = texture(tex, uv_to_region(texRegion, tc));
vec4 shadowfrag = vec4(vec3(0), a * color.a * texel.r);
fragColor = textfrag;
fragColor = mix(shadowfrag, textfrag, sqrt(textfrag.a));
tc_overlay = clamp(tc_overlay,0.01,0.99); // The overlay coordinates are outside of [0,1] in the padding region, so we make sure there are no wrap around artifacts when a bit of text is distorted to this region.
fragColor.a *= clamp((texture(trans, tc_overlay).r + 0.5) * 2.5 * t-0.5, 0.0, 1.0);
}

View file

@ -0,0 +1,2 @@
glsl_objects = text_example.vert text_stagetext.frag

View file

@ -18,7 +18,7 @@
CurrentBGM current_bgm = { .name = NULL };
static char *saved_bgm;
static Hashtable *sfx_volumes;
static ht_str2int_t sfx_volumes;
static struct enqueued_sound {
LIST_INTERFACE(struct enqueued_sound);
@ -173,7 +173,6 @@ static void bgm_cfg_volume_callback(ConfigIndex idx, ConfigValue v) {
}
static bool store_sfx_volume(const char *key, const char *val, void *data) {
Hashtable *ht = data;
int vol = atoi(val);
if(vol < 0 || vol > 128) {
@ -184,14 +183,15 @@ static bool store_sfx_volume(const char *key, const char *val, void *data) {
log_debug("Default volume for %s is now %i", key, vol);
if(vol != DEFAULT_SFX_VOLUME) {
hashtable_set_string(ht, key, (void*)(intptr_t)vol);
ht_set(&sfx_volumes, key, vol);
}
return true;
}
static void load_config_files(void) {
sfx_volumes = hashtable_new_stringkeys();
parse_keyvalue_file_cb(SFX_PATH_PREFIX "volumes.conf", store_sfx_volume, sfx_volumes);
ht_create(&sfx_volumes);
parse_keyvalue_file_cb(SFX_PATH_PREFIX "volumes.conf", store_sfx_volume, NULL);
}
static inline char* get_bgm_desc(char *name) {
@ -201,13 +201,7 @@ static inline char* get_bgm_desc(char *name) {
}
int get_default_sfx_volume(const char *sfx) {
void *v = hashtable_get_string(sfx_volumes, sfx);
if(v != NULL) {
return (intptr_t)v;
}
return DEFAULT_SFX_VOLUME;
return ht_get(&sfx_volumes, sfx, DEFAULT_SFX_VOLUME);
}
void resume_bgm(void) {
@ -315,9 +309,5 @@ void audio_init(void) {
void audio_shutdown(void) {
audio_backend_shutdown();
if(sfx_volumes) {
hashtable_free(sfx_volumes);
sfx_volumes = NULL;
}
ht_destroy(&sfx_volumes);
}

View file

@ -39,24 +39,30 @@ Boss* create_boss(char *name, char *ani, char *dialog, complex pos) {
return buf;
}
void draw_boss_text(Alignment align, float x, float y, const char *text, Font *fnt, Color clr) {
Color color_saved = r_color_current();
void draw_boss_text(Alignment align, float x, float y, const char *text, const char *fnt, Color clr) {
ShaderProgram *sh_prev = r_shader_current();
r_shader("text_default");
text_draw(text, &(TextParams) {
.pos = { x + 1, y + 1 },
.color = derive_color(rgb(0, 0, 0), CLRMASK_A, clr),
.font = fnt,
.align = align,
});
r_shader_standard();
r_color(derive_color(rgb(0, 0, 0), CLRMASK_A, clr));
draw_text(align, x+1, y+1, text, fnt);
r_color(clr);
draw_text(align, x, y, text, fnt);
r_shader("sprite_default");
r_color(color_saved);
text_draw(text, &(TextParams) {
.pos = { x, y },
.color = clr,
.font = fnt,
.align = align,
});
r_shader_ptr(sh_prev);
}
void spell_opening(Boss *b, int time) {
complex x0 = VIEWPORT_W/2+I*VIEWPORT_H/3.5;
float f = clamp((time-40.)/60.,0,1);
complex x = x0 + (VIEWPORT_W+I*35 - x0) * f*(f+1)*0.5;
int strw = stringwidth(b->current->name,_fonts.standard);
int strw = text_width(get_font("standard"), b->current->name, 0);
bool cullcap_saved = r_capability_current(RCAP_CULL_FACE);
r_disable(RCAP_CULL_FACE);
@ -66,7 +72,7 @@ void spell_opening(Boss *b, int time) {
float scale = f+1.*(1-f)*(1-f)*(1-f);
r_mat_scale(scale,scale,1);
r_mat_rotate_deg(360*f,1,1,0);
draw_boss_text(AL_Right, strw/2*(1-f), 0, b->current->name, _fonts.standard, rgb(1, 1, 1));
draw_boss_text(ALIGN_RIGHT, strw/2*(1-f), 0, b->current->name, "standard", rgb(1, 1, 1));
r_mat_pop();
r_capability(RCAP_CULL_FACE, cullcap_saved);
@ -276,7 +282,7 @@ static void ent_draw_boss(EntityInterface *ent) {
if(boss->current->type == AT_Move && global.frames - boss->current->starttime > 0 && boss_attack_is_final(boss, boss->current))
return;
draw_boss_text(AL_Left, 10, 20, boss->name, _fonts.standard, rgb(1, 1, 1));
draw_boss_text(ALIGN_LEFT, 10, 20, boss->name, "standard", rgb(1, 1, 1));
if(!boss->current)
return;
@ -298,16 +304,19 @@ static void ent_draw_boss(EntityInterface *ent) {
}
snprintf(buf, sizeof(buf), "%.2f", remaining);
draw_boss_text(AL_Center, VIEWPORT_W - 24, 10, buf, _fonts.standard, textclr);
draw_boss_text(ALIGN_CENTER, VIEWPORT_W - 24, 10, buf, "standard", textclr);
StageProgress *p = get_spellstage_progress(boss->current, NULL, false);
if(p) {
float a = clamp((global.frames - boss->current->starttime - 60) / 60.0, 0, 1);
snprintf(buf, sizeof(buf), "%u / %u", p->num_cleared, p->num_played);
draw_boss_text(AL_Right,
VIEWPORT_W + stringwidth(buf, _fonts.small) * pow(1 - a, 2),
35 + stringheight(buf, _fonts.small),
buf, _fonts.small, rgba(1, 1, 1, a)
Font *font = get_font("small");
draw_boss_text(ALIGN_RIGHT,
VIEWPORT_W + text_width(font, buf, 0) * pow(1 - a, 2),
35 + text_height(font, buf, 0),
buf, "small", rgba(1, 1, 1, a)
);
}

View file

@ -236,11 +236,11 @@ static double entry_height(CreditsEntry *e, double *head, double *body) {
if(*(e->data[0]) == '*') {
total += *head = get_tex("yukkureimu")->h * CREDITS_YUKKURI_SCALE;
} else {
total += *head = font_line_spacing(_fonts.mainmenu);
total += *head = font_get_lineskip(get_font("big"));
}
if(e->lines > 1) {
total += *body += (e->lines - 0.5) * font_line_spacing(_fonts.standard);
total += *body += (e->lines - 0.5) * font_get_lineskip(get_font("standard"));
}
}
@ -323,9 +323,14 @@ static void credits_draw_entry(CreditsEntry *e) {
r_mat_pop();
r_mat_translate(0, yukkuri_spr->h * CREDITS_YUKKURI_SCALE * 0.5, 0);
} else {
Font *font = i ? _fonts.standard : _fonts.mainmenu;
draw_text(AL_Center, 0, 0, e->data[i], font);
r_mat_translate(0, font_line_spacing(font), 0);
Font *font = get_font(i ? "standard" : "big");
r_shader("text_default");
text_draw(e->data[i], &(TextParams) {
.align = ALIGN_CENTER,
.font_ptr = font,
});
r_shader_standard();
r_mat_translate(0, font_get_lineskip(font), 0);
}
}

View file

@ -120,7 +120,11 @@ void draw_dialog(Dialog *dialog) {
if(dialog->messages[dialog->pos].side == Right)
r_color3(0.6,0.6,1);
draw_text_auto_wrapped(AL_Center, VIEWPORT_W/2, VIEWPORT_H-110, dialog->messages[dialog->pos].msg, VIEWPORT_W * 0.85, _fonts.standard);
r_shader("text_default");
text_draw_wrapped(dialog->messages[dialog->pos].msg, VIEWPORT_W * 0.85, &(TextParams) {
.pos = { VIEWPORT_W/2, VIEWPORT_H-110 },
.align = ALIGN_CENTER
});
if(dialog->messages[dialog->pos].side == Right)
r_color3(1,1,1);

View file

@ -161,7 +161,13 @@ static void ending_draw(Ending *e) {
draw_sprite_p(SCREEN_W/2, SCREEN_H/2, e->entries[e->pos].sprite);
}
draw_text_auto_wrapped(AL_Center, SCREEN_W/2, VIEWPORT_H*4/5, e->entries[e->pos].msg, SCREEN_W * 0.85, _fonts.standard);
r_shader("text_default");
text_draw_wrapped(e->entries[e->pos].msg, SCREEN_W * 0.85, &(TextParams) {
.pos = { SCREEN_W/2, VIEWPORT_H*4/5 },
.align = ALIGN_CENTER,
});
r_shader_standard();
r_color4(1,1,1,1);
}

View file

@ -8,530 +8,18 @@
#include "taisei.h"
#define HT_IMPL
#include "hashtable.h"
#include "list.h"
#include "util.h"
#include <string.h>
#include <zlib.h>
#include <stdio.h>
#include <SDL_mutex.h>
uint32_t (*htutil_hashfunc_string)(uint32_t crc, const char *str);
// #define HT_USE_MUTEX
typedef struct HashtableElement {
LIST_INTERFACE(struct HashtableElement);
void *data;
void *key;
hash_t hash;
} HashtableElement;
struct Hashtable {
HashtableElement **table;
HTCmpFunc cmp_func;
HTHashFunc hash_func;
HTCopyFunc copy_func;
HTFreeFunc free_func;
size_t num_elements;
size_t table_size;
size_t hash_mask;
struct {
SDL_mutex *mutex;
SDL_cond *cond;
uint readers;
bool writing;
} sync;
};
typedef struct HashtableIterator {
Hashtable *hashtable;
size_t bucketnum;
HashtableElement *elem;
} HashtableIterator;
/*
* Generic functions
*/
Hashtable* hashtable_new(HTCmpFunc cmp_func, HTHashFunc hash_func, HTCopyFunc copy_func, HTFreeFunc free_func) {
Hashtable *ht = malloc(sizeof(Hashtable));
size_t size = HT_MIN_SIZE;
if(!cmp_func) {
cmp_func = hashtable_cmpfunc_ptr;
}
if(!copy_func) {
copy_func = hashtable_copyfunc_ptr;
}
ht->table = calloc(size, sizeof(HashtableElement*));
ht->table_size = size;
ht->hash_mask = size - 1;
ht->num_elements = 0;
ht->cmp_func = cmp_func;
ht->hash_func = hash_func;
ht->copy_func = copy_func;
ht->free_func = free_func;
ht->sync.writing = false;
ht->sync.readers = 0;
ht->sync.mutex = SDL_CreateMutex();
ht->sync.cond = SDL_CreateCond();
assert(ht->hash_func != NULL);
return ht;
}
static void hashtable_begin_write(Hashtable *ht) {
SDL_LockMutex(ht->sync.mutex);
while(ht->sync.writing || ht->sync.readers) {
SDL_CondWait(ht->sync.cond, ht->sync.mutex);
}
ht->sync.writing = true;
SDL_UnlockMutex(ht->sync.mutex);
}
static void hashtable_end_write(Hashtable *ht) {
SDL_LockMutex(ht->sync.mutex);
ht->sync.writing = false;
SDL_CondBroadcast(ht->sync.cond);
SDL_UnlockMutex(ht->sync.mutex);
}
static void hashtable_begin_read(Hashtable *ht) {
SDL_LockMutex(ht->sync.mutex);
while(ht->sync.writing) {
SDL_CondWait(ht->sync.cond, ht->sync.mutex);
}
++ht->sync.readers;
SDL_UnlockMutex(ht->sync.mutex);
}
static void hashtable_end_read(Hashtable *ht) {
SDL_LockMutex(ht->sync.mutex);
if(!--ht->sync.readers) {
SDL_CondSignal(ht->sync.cond);
}
SDL_UnlockMutex(ht->sync.mutex);
}
void hashtable_lock(Hashtable *ht) {
assert(ht != NULL);
hashtable_begin_read(ht);
}
void hashtable_unlock(Hashtable *ht) {
assert(ht != NULL);
hashtable_end_read(ht);
}
static void* hashtable_delete_callback(List **vlist, List *velem, void *vht) {
Hashtable *ht = vht;
HashtableElement *elem = (HashtableElement*)velem;
if(ht->free_func) {
ht->free_func(elem->key);
}
free(list_unlink(vlist, velem));
return NULL;
}
static void hashtable_unset_all_internal(Hashtable *ht) {
for(size_t i = 0; i < ht->table_size; ++i) {
list_foreach((ht->table + i), hashtable_delete_callback, ht);
}
ht->num_elements = 0;
}
void hashtable_unset_all(Hashtable *ht) {
assert(ht != NULL);
hashtable_begin_write(ht);
hashtable_unset_all_internal(ht);
hashtable_end_write(ht);
}
void hashtable_free(Hashtable *ht) {
if(!ht) {
return;
}
hashtable_unset_all(ht);
SDL_DestroyCond(ht->sync.cond);
SDL_DestroyMutex(ht->sync.mutex);
free(ht->table);
free(ht);
}
static HashtableElement* hashtable_get_internal(Hashtable *ht, void *key, hash_t hash) {
HashtableElement *elems = ht->table[hash & ht->hash_mask];
for(HashtableElement *e = elems; e; e = e->next) {
if(hash == e->hash && ht->cmp_func(key, e->key)) {
return e;
}
}
return NULL;
}
void* hashtable_get(Hashtable *ht, void *key) {
assert(ht != NULL);
hash_t hash = ht->hash_func(key);
HashtableElement *elem;
void *data;
hashtable_begin_read(ht);
elem = hashtable_get_internal(ht, key, hash);
data = elem ? elem->data : NULL;
hashtable_end_read(ht);
return data;
}
void* hashtable_get_unsafe(Hashtable *ht, void *key) {
assert(ht != NULL);
hash_t hash = ht->hash_func(key);
HashtableElement *elem;
void *data;
elem = hashtable_get_internal(ht, key, hash);
data = elem ? elem->data : NULL;
return data;
}
static bool hashtable_set_internal(Hashtable *ht, HashtableElement **table, size_t hash_mask, hash_t hash, void *key, void *data, void* (*datafunc)(void*), bool allow_overwrite, void **val) {
size_t idx = hash & hash_mask;
HashtableElement *elems = table[idx], *elem;
void *result = NULL;
for(HashtableElement *e = elems; e; e = e->next) {
if(hash == e->hash && ht->cmp_func(key, e->key)) {
if(!allow_overwrite) {
if(val != NULL) {
*val = e->data;
}
return result;
}
if(ht->free_func) {
ht->free_func(e->key);
}
free(list_unlink(&elems, e));
ht->num_elements--;
break;
}
}
if(datafunc != NULL) {
data = datafunc(data);
}
if(val != NULL) {
*val = data;
}
if(data) {
elem = malloc(sizeof(HashtableElement));
ht->copy_func(&elem->key, key);
elem->hash = ht->hash_func(elem->key);
elem->data = data;
list_push(&elems, elem);
ht->num_elements++;
result = data;
}
table[idx] = elems;
return result;
}
static void hashtable_resize(Hashtable *ht, size_t new_size) {
assert(new_size != ht->table_size);
HashtableElement **new_table = calloc(new_size, sizeof(HashtableElement*));
size_t new_hash_mask = new_size - 1;
for(size_t i = 0; i < ht->table_size; ++i) {
for(HashtableElement *e = ht->table[i]; e; e = e->next) {
hashtable_set_internal(ht, new_table, new_hash_mask, e->hash, e->key, e->data, NULL, true, NULL);
}
}
hashtable_unset_all_internal(ht);
free(ht->table);
ht->table = new_table;
log_debug("Resized hashtable at %p: %"PRIuMAX" -> %"PRIuMAX"",
(void*)ht, (uintmax_t)ht->table_size, (uintmax_t)new_size);
// hashtable_print_stringkeys(ht);
ht->table_size = new_size;
ht->hash_mask = new_hash_mask;
}
void hashtable_set(Hashtable *ht, void *key, void *data) {
assert(ht != NULL);
hash_t hash = ht->hash_func(key);
hashtable_begin_write(ht);
hashtable_set_internal(ht, ht->table, ht->hash_mask, hash, key, data, NULL, true, NULL);
if(ht->num_elements == ht->table_size) {
hashtable_resize(ht, ht->table_size * 2);
}
hashtable_end_write(ht);
}
bool hashtable_try_set(Hashtable *ht, void *key, void *data, void* (*datafunc)(void*), void **val) {
assert(ht != NULL);
hash_t hash = ht->hash_func(key);
hashtable_begin_write(ht);
bool result = hashtable_set_internal(ht, ht->table, ht->hash_mask, hash, key, data, datafunc, false, val);
if(ht->num_elements == ht->table_size) {
hashtable_resize(ht, ht->table_size * 2);
}
hashtable_end_write(ht);
return result;
}
void hashtable_unset(Hashtable *ht, void *key) {
hashtable_set(ht, key, NULL);
}
void hashtable_unset_deferred(Hashtable *ht, void *key, ListContainer **list) {
assert(ht != NULL);
ListContainer *c = list_push(list, list_wrap_container(NULL));
ht->copy_func(&c->data, key);
}
void hashtable_unset_deferred_now(Hashtable *ht, ListContainer **list) {
ListContainer *next;
assert(ht != NULL);
for(ListContainer *c = *list; c; c = next) {
next = c->next;
hashtable_unset(ht, c->data);
if(ht->free_func) {
ht->free_func(c->data);
}
free(list_unlink(list, c));
}
}
/*
* Iteration functions
*/
void* hashtable_foreach(Hashtable *ht, HTIterCallback callback, void *arg) {
assert(ht != NULL);
void *ret = NULL;
hashtable_begin_read(ht);
for(size_t i = 0; i < ht->table_size; ++i) {
for(HashtableElement *e = ht->table[i]; e; e = e->next) {
ret = callback(e->key, e->data, arg);
if(ret) {
hashtable_end_read(ht);
return ret;
}
}
}
hashtable_end_read(ht);
return ret;
}
HashtableIterator* hashtable_iter(Hashtable *ht) {
assert(ht != NULL);
HashtableIterator *iter = malloc(sizeof(HashtableIterator));
iter->hashtable = ht;
iter->bucketnum = (size_t)-1;
return iter;
}
bool hashtable_iter_next(HashtableIterator *iter, void **out_key, void **out_data) {
Hashtable *ht = iter->hashtable;
if(iter->bucketnum == (size_t)-1) {
iter->bucketnum = 0;
iter->elem = ht->table[iter->bucketnum];
void htutil_init(void) {
if(SDL_HasSSE42()) {
log_info("Using SSE4.2-accelerated CRC32 as the string hash function");
htutil_hashfunc_string = crc32str_sse42;
} else {
iter->elem = iter->elem->next;
}
while(!iter->elem) {
if(++iter->bucketnum == ht->table_size) {
free(iter);
return false;
}
iter->elem = ht->table[iter->bucketnum];
}
if(out_key) {
*out_key = iter->elem->key;
}
if(out_data) {
*out_data = iter->elem->data;
}
return true;
}
/*
* Convenience functions for hashtables with string keys
*/
bool hashtable_cmpfunc_string(void *str1, void *str2) {
return !strcmp((const char*)str1, (const char*)(str2));
}
hash_t hashtable_hashfunc_string(void *vstr) {
return crc32str(0, (const char*)vstr);
}
hash_t hashtable_hashfunc_string_sse42(void *vstr) {
return crc32str_sse42(0, (const char*)vstr);
}
void hashtable_copyfunc_string(void **dst, void *src) {
*dst = malloc(strlen((char*)src) + 1);
strcpy(*dst, src);
}
// #define hashtable_freefunc_string free
Hashtable* hashtable_new_stringkeys(void) {
return hashtable_new(
hashtable_cmpfunc_string,
SDL_HasSSE42() ? hashtable_hashfunc_string_sse42 : hashtable_hashfunc_string,
hashtable_copyfunc_string,
hashtable_freefunc_string
);
}
void* hashtable_get_string(Hashtable *ht, const char *key) {
return hashtable_get(ht, (void*)key);
}
void hashtable_set_string(Hashtable *ht, const char *key, void *data) {
hashtable_set(ht, (void*)key, data);
}
void hashtable_unset_string(Hashtable *ht, const char *key) {
hashtable_unset(ht, (void*)key);
}
/*
* Misc convenience functions
*/
void* hashtable_iter_free_data(void *key, void *data, void *arg) {
free(data);
return NULL;
}
bool hashtable_cmpfunc_ptr(void *p1, void *p2) {
return p1 == p2;
}
void hashtable_copyfunc_ptr(void **dst, void *src) {
*dst = src;
}
hash_t hashtable_hashfunc_ptr(void *val) {
hash_t x = (uintptr_t)val & ((1u << 31u) - 1u);
x = ((x >> 16) ^ x) * 0x45d9f3b;
x = ((x >> 16) ^ x) * 0x45d9f3b;
x = (x >> 16) ^ x;
return x;
}
/*
* Diagnostic functions
*/
void hashtable_get_stats(Hashtable *ht, HashtableStats *stats) {
assert(ht != NULL);
assert(stats != NULL);
memset(stats, 0, sizeof(HashtableStats));
for(size_t i = 0; i < ht->table_size; ++i) {
int elems = 0;
for(HashtableElement *e = ht->table[i]; e; e = e->next) {
++elems;
++stats->num_elements;
}
if(elems > 1) {
stats->collisions += elems - 1;
}
if(!elems) {
++stats->free_buckets;
} else if(elems > stats->max_per_bucket) {
stats->max_per_bucket = elems;
}
log_info("Using software fallback CRC32 as the string hash function");
htutil_hashfunc_string = crc32str;
}
}
size_t hashtable_get_approx_overhead(Hashtable *ht) {
size_t o = sizeof(Hashtable) + sizeof(HashtableElement*) * ht->table_size;
for(HashtableIterator *i = hashtable_iter(ht); hashtable_iter_next(i, NULL, NULL);) {
o += sizeof(HashtableElement);
}
return o;
}
void hashtable_print_stringkeys(Hashtable *ht) {
HashtableStats stats;
hashtable_get_stats(ht, &stats);
log_debug("------ %p:", (void*)ht);
for(size_t i = 0; i < ht->table_size; ++i) {
log_debug("[bucket %"PRIuMAX"] %p", (uintmax_t)i, (void*)ht->table[i]);
for(HashtableElement *e = ht->table[i]; e; e = e->next) {
log_debug(" -- %s (%"PRIuMAX"): %p", (char*)e->key, (uintmax_t)e->hash, e->data);
}
}
log_debug(
"%i total elements, %i unused buckets, %i collisions, max %i elems per bucket, %lu approx overhead",
stats.num_elements, stats.free_buckets, stats.collisions, stats.max_per_bucket,
(ulong)hashtable_get_approx_overhead(ht)
);
}

View file

@ -9,67 +9,55 @@
#pragma once
#include "taisei.h"
#include <stdlib.h>
#include "list.h"
#define HT_MIN_SIZE 16
typedef struct Hashtable Hashtable;
typedef struct HashtableIterator HashtableIterator;
typedef struct HashtableStats HashtableStats;
/*
* hash_t
*
* Type of the hash values.
*/
typedef uint32_t hash_t;
struct HashtableStats {
uint free_buckets;
uint collisions;
uint max_per_bucket;
uint num_elements;
};
/*
* htutil_init
*
* One-time setup function.
*/
void htutil_init(void);
typedef bool (*HTCmpFunc)(void *key1, void *key2);
typedef hash_t (*HTHashFunc)(void *key);
typedef void (*HTCopyFunc)(void **dstkey, void *srckey);
typedef void (*HTFreeFunc)(void *key);
typedef void* (*HTIterCallback)(void *key, void *data, void *arg);
/*
* htutil_hashfunc_uint32
*
* Hash function for 32-bit integers
* NOTE: assuming hash_t stays uint32_t, this function has no collisions.
*/
static inline attr_must_inline hash_t htutil_hashfunc_uint32(uint32_t x) {
x = ((x >> 16) ^ x) * 0x45d9f3b;
x = ((x >> 16) ^ x) * 0x45d9f3b;
x = (x >> 16) ^ x;
return (hash_t)x;
}
Hashtable* hashtable_new(HTCmpFunc cmp_func, HTHashFunc hash_func, HTCopyFunc copy_func, HTFreeFunc free_func);
void hashtable_free(Hashtable *ht);
void* hashtable_get(Hashtable *ht, void *key) attr_hot;
void* hashtable_get_unsafe(Hashtable *ht, void *key) attr_hot;
void hashtable_set(Hashtable *ht, void *key, void *data);
bool hashtable_try_set(Hashtable *ht, void *key, void *data, void* (*datafunc)(void*), void **val);
void hashtable_unset(Hashtable *ht, void *key);
void hashtable_unset_deferred(Hashtable *ht, void *key, ListContainer **list);
void hashtable_unset_deferred_now(Hashtable *ht, ListContainer **list);
void hashtable_unset_all(Hashtable *ht);
/*
* htutil_hashfunc_uint64
*
* Hash function for 64-bit integers.
*/
static inline attr_must_inline hash_t htutil_hashfunc_uint64(uint64_t x) {
x = (x ^ (x >> 30)) * UINT64_C(0xbf58476d1ce4e5b9);
x = (x ^ (x >> 27)) * UINT64_C(0x94d049bb133111eb);
x = x ^ (x >> 31);
return (hash_t)x;
}
void* hashtable_foreach(Hashtable *ht, HTIterCallback callback, void *arg);
/*
* htutil_hashfunc_string
*
* Hash function for null-terminated strings.
*/
extern uint32_t (*htutil_hashfunc_string)(uint32_t crc, const char *str);
// THIS IS NOT THREAD-SAFE. You have to use hashtable_lock/unlock to make it so.
HashtableIterator* hashtable_iter(Hashtable *ht);
bool hashtable_iter_next(HashtableIterator *iter, void **out_key, void **out_data);
// Import public declarations for the predefined hashtable types.
#define HT_DECL
#include "hashtable_predefs.inc.h"
bool hashtable_cmpfunc_string(void *str1, void *str2) attr_hot;
hash_t hashtable_hashfunc_string(void *vstr) attr_hot;
hash_t hashtable_hashfunc_string_sse42(void *vstr) attr_hot;
void hashtable_copyfunc_string(void **dst, void *src);
#define hashtable_freefunc_string free
Hashtable* hashtable_new_stringkeys(void);
void* hashtable_get_string(Hashtable *ht, const char *key);
void hashtable_set_string(Hashtable *ht, const char *key, void *data);
void hashtable_unset_string(Hashtable *ht, const char *key);
void hashtable_unset_deferred_string(Hashtable *ht, const char *key);
void* hashtable_iter_free_data(void *key, void *data, void *arg);
bool hashtable_cmpfunc_ptr(void *p1, void *p2);
void hashtable_copyfunc_ptr(void **dst, void *src);
hash_t hashtable_hashfunc_ptr(void *val);
void hashtable_print_stringkeys(Hashtable *ht);
size_t hashtable_get_approx_overhead(Hashtable *ht);
void hashtable_get_stats(Hashtable *ht, HashtableStats *stats);
void hashtable_lock(Hashtable *ht);
void hashtable_unlock(Hashtable *ht);
// NOTE: For the hashtable API, see hashtable.inc.h
// NOTE: For type-generic wrappers around the API, see hashtable_predefs.inc.h

1045
src/hashtable.inc.h Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,10 @@
#ifdef _HT_IMPL
#define HT_IMPL
#endif
#ifdef _HT_DECL
#define HT_DECL
#endif
#include "hashtable.inc.h"

191
src/hashtable_predefs.inc.h Normal file
View file

@ -0,0 +1,191 @@
// Sets up the predefined hashtable types.
// Add more as necessary.
#ifdef HT_IMPL
#define _HT_IMPL
#endif
#ifdef HT_DECL
#define _HT_DECL
#endif
/*
* str2ptr
*
* Maps strings to void pointers.
*/
#define HT_SUFFIX str2ptr
#define HT_KEY_TYPE char*
#define HT_VALUE_TYPE void*
#define HT_FUNC_FREE_KEY(key) free(key)
#define HT_FUNC_KEYS_EQUAL(key1, key2) (!strcmp(key1, key2))
#define HT_FUNC_HASH_KEY(key) htutil_hashfunc_string(0, key)
#define HT_FUNC_COPY_KEY(dst, src) (*(dst) = strdup(src))
#define HT_KEY_CONST
#define HT_VALUE_CONST
#include "hashtable_incproxy.inc.h"
/*
* str2ptr_ts
*
* Maps strings to void pointers (thread-safe).
*/
#define HT_SUFFIX str2ptr_ts
#define HT_KEY_TYPE char*
#define HT_VALUE_TYPE void*
#define HT_FUNC_FREE_KEY(key) free(key)
#define HT_FUNC_KEYS_EQUAL(key1, key2) (!strcmp(key1, key2))
#define HT_FUNC_HASH_KEY(key) htutil_hashfunc_string(0, key)
#define HT_FUNC_COPY_KEY(dst, src) (*(dst) = strdup(src))
#define HT_KEY_CONST
#define HT_VALUE_CONST
#define HT_THREAD_SAFE
#include "hashtable_incproxy.inc.h"
/*
* str2int
*
* Maps strings to 64-bit integers.
*/
#define HT_SUFFIX str2int
#define HT_KEY_TYPE char*
#define HT_VALUE_TYPE int64_t
#define HT_FUNC_FREE_KEY(key) free(key)
#define HT_FUNC_KEYS_EQUAL(key1, key2) (!strcmp(key1, key2))
#define HT_FUNC_HASH_KEY(key) htutil_hashfunc_string(0, key)
#define HT_FUNC_COPY_KEY(dst, src) (*(dst) = strdup(src))
#define HT_KEY_CONST
#include "hashtable_incproxy.inc.h"
/*
* str2int_ts
*
* Maps strings to 64-bit integers.
*/
#define HT_SUFFIX str2int_ts
#define HT_KEY_TYPE char*
#define HT_VALUE_TYPE int64_t
#define HT_FUNC_FREE_KEY(key) free(key)
#define HT_FUNC_KEYS_EQUAL(key1, key2) (!strcmp(key1, key2))
#define HT_FUNC_HASH_KEY(key) htutil_hashfunc_string(0, key)
#define HT_FUNC_COPY_KEY(dst, src) (*(dst) = strdup(src))
#define HT_KEY_CONST
#define HT_THREAD_SAFE
#include "hashtable_incproxy.inc.h"
/*
* int2int
*
* Maps 64-bit integers to 64-bit integers.
*/
#define HT_SUFFIX int2int
#define HT_KEY_TYPE int64_t
#define HT_VALUE_TYPE int64_t
#define HT_FUNC_HASH_KEY(key) htutil_hashfunc_uint64((uint64_t)(key))
#include "hashtable_incproxy.inc.h"
/*
* int2int_ts
*
* Maps 64-bit integers to 64-bit integers (thread-safe).
*/
#define HT_SUFFIX int2int_ts
#define HT_KEY_TYPE int64_t
#define HT_VALUE_TYPE int64_t
#define HT_FUNC_HASH_KEY(key) htutil_hashfunc_uint64((uint64_t)(key))
#define HT_THREAD_SAFE
#include "hashtable_incproxy.inc.h"
/*
* C11 generic selection witchcraft.