diff --git a/src/raylib.h b/src/raylib.h index c57efc2e1..90ff40087 100644 --- a/src/raylib.h +++ b/src/raylib.h @@ -320,6 +320,7 @@ typedef struct Font { Texture2D texture; // Texture atlas containing the glyphs Rectangle *recs; // Rectangles in texture for the glyphs GlyphInfo *glyphs; // Glyphs info data + void *glyphLookup; // Interal glyphs lookup map } Font; // Camera, defines position/orientation in 3d space diff --git a/src/rtext.c b/src/rtext.c index 6a87fe3db..8a44530a1 100644 --- a/src/rtext.c +++ b/src/rtext.c @@ -155,6 +155,26 @@ extern void LoadFontDefault(void); extern void UnloadFontDefault(void); #endif +// Glyph hashmap related declarations +typedef struct { + int key; + int value; + bool active; +} GlyphMapBucket; + +typedef struct { + int count; + int capacity; + GlyphMapBucket *buckets; +} GlyphLookupMap; + +static unsigned int HashCodepoint(int key); +static GlyphLookupMap *CreateGlyphLookupMap(int capacity); +static void DestroyGlyphLookupMap(GlyphLookupMap *map); +static bool MapInsert(GlyphLookupMap *map, int key, int value); +static bool MapGet(GlyphLookupMap *map, int key, int *value); +static void GenFontGlyphLookupMap(Font *font); + //---------------------------------------------------------------------------------- // Module Functions Definition //---------------------------------------------------------------------------------- @@ -324,12 +344,21 @@ extern void LoadFontDefault(void) defaultFont.baseSize = (int)defaultFont.recs[0].height; + // Create a lookup map for the font + GenFontGlyphLookupMap(&defaultFont); + TRACELOG(LOG_INFO, "FONT: Default font loaded successfully (%i glyphs)", defaultFont.glyphCount); } // Unload raylib default font extern void UnloadFontDefault(void) { + if (defaultFont.glyphLookup != NULL) + { + DestroyGlyphLookupMap((GlyphLookupMap *)defaultFont.glyphLookup); + defaultFont.glyphLookup = NULL; + } + for (int i = 0; i < defaultFont.glyphCount; i++) UnloadImage(defaultFont.glyphs[i].image); if (isGpuReady) UnloadTexture(defaultFont.texture); RL_FREE(defaultFont.glyphs); @@ -541,6 +570,9 @@ Font LoadFontFromImage(Image image, Color key, int firstChar) font.glyphs[i].image = ImageFromImage(fontClear, tempCharRecs[i]); } + // Create a lookup map for the font + GenFontGlyphLookupMap(&font); + UnloadImage(fontClear); // Unload processed image once converted to texture font.baseSize = (int)font.recs[0].height; @@ -558,6 +590,7 @@ Font LoadFontFromMemory(const char *fileType, const unsigned char *fileData, int font.baseSize = fontSize; font.glyphPadding = 0; + font.glyphLookup = NULL; #if defined(SUPPORT_FILEFORMAT_TTF) if (TextIsEqual(fileExtLower, ".ttf") || @@ -596,6 +629,8 @@ Font LoadFontFromMemory(const char *fileType, const unsigned char *fileData, int UnloadImage(atlas); + GenFontGlyphLookupMap(&font); + TRACELOG(LOG_INFO, "FONT: Data loaded successfully (%i pixel size | %i glyphs)", font.baseSize, font.glyphCount); } else font = GetFontDefault(); @@ -1007,6 +1042,11 @@ void UnloadFont(Font font) // NOTE: Make sure font is not default font (fallback) if (font.texture.id != GetFontDefault().texture.id) { + if (font.glyphLookup != NULL) + { + DestroyGlyphLookupMap((GlyphLookupMap *)font.glyphLookup); + font.glyphLookup = NULL; + } UnloadFontData(font.glyphs, font.glyphCount); if (isGpuReady) UnloadTexture(font.texture); RL_FREE(font.recs); @@ -1395,33 +1435,45 @@ Vector2 MeasureTextEx(Font font, const char *text, float fontSize, float spacing // NOTE: If codepoint is not found in the font it fallbacks to '?' int GetGlyphIndex(Font font, int codepoint) { - int index = 0; - if (!IsFontValid(font)) return index; - -#define SUPPORT_UNORDERED_CHARSET -#if defined(SUPPORT_UNORDERED_CHARSET) - int fallbackIndex = 0; // Get index of fallback glyph '?' + if (!IsFontValid(font)) return 0; - // Look for character index in the unordered charset - for (int i = 0; i < font.glyphCount; i++) + // The font hashmap searching algorithm + if (font.glyphLookup != NULL) { - if (font.glyphs[i].value == 63) fallbackIndex = i; + int index = 0; + GlyphLookupMap *map = (GlyphLookupMap *)font.glyphLookup; - if (font.glyphs[i].value == codepoint) + if (MapGet(map, codepoint, &index)) { - index = i; - break; + return index; // found, can now return + } + else + { + // codepoint not in font, try to find the fallback character '?' + if (MapGet(map, '?', &index)) return index; + else return 0; // fallback to the first character } } - if ((index == 0) && (font.glyphs[0].value != codepoint)) index = fallbackIndex; -#else - index = codepoint - 32; -#endif + // If a font somehow gets loaded without a map being created, this is the fallback. + // I recommend making a bug report if you ever get this error message, or looking at how you're creating your fonts!! + TRACELOG(LOG_WARNING, "FONT: GetGlyphIndex() is using slow linear search. Font might be missing a lookup map."); - return index; + #if defined(SUPPORT_UNORDERED_CHARSET) + int fallbackIndex = 0; + for (int i = 0; i < font.glyphCount; i++) + { + if (font.glyphs[i].value == '?') fallbackIndex = i; + if (font.glyphs[i].value == codepoint) return i; + } + return fallbackIndex; + #else + // Assumes ASCII-based ordered charset + return (codepoint - 32); + #endif } + // Get glyph font info data for a codepoint (unicode character) // NOTE: If codepoint is not found in the font it fallbacks to '?' GlyphInfo GetGlyphInfo(Font font, int codepoint) @@ -2344,6 +2396,7 @@ static Font LoadBMFont(const char *fileName) int fontSize = 0; int glyphCount = 0; + font.glyphLookup = NULL; int imWidth = 0; int imHeight = 0; @@ -2483,6 +2536,8 @@ static Font LoadBMFont(const char *fileName) } } + GenFontGlyphLookupMap(&font); + UnloadImage(fullFont); UnloadFileText(fileText); @@ -2498,6 +2553,115 @@ static Font LoadBMFont(const char *fileName) } #endif +// My implementation of fnv-1a for quick hash tables +// If you change this, I strongly recommend using the documentation below! +// http://www.isthe.com/chongo/tech/comp/fnv/#FNV-1a + +static unsigned int HashCodepoint(int key) +{ + unsigned int hash = 2166136261u; + char *p = (char *)&key; + for (int i = 0; i < sizeof(int); i++) + { + hash ^= (unsigned int)p[i]; + hash *= 16777619u; + } + return hash; +} + +static GlyphLookupMap *CreateGlyphLookupMap(int capacity) +{ + GlyphLookupMap *map = (GlyphLookupMap *)RL_CALLOC(1, sizeof(GlyphLookupMap)); + if (map == NULL) return NULL; + + map->capacity = capacity; + map->buckets = (GlyphMapBucket *)RL_CALLOC(capacity, sizeof(GlyphMapBucket)); + if (map->buckets == NULL) + { + RL_FREE(map); + return NULL; + } + return map; +} + +static void DestroyGlyphLookupMap(GlyphLookupMap *map) +{ + if (map == NULL) return; + RL_FREE(map->buckets); + RL_FREE(map); +} + +static bool MapInsert(GlyphLookupMap *map, int key, int value) +{ + if (map == NULL || map->buckets == NULL) return false; + + unsigned int index = HashCodepoint(key) & (map->capacity - 1); + + for (int i = 0; i < map->capacity; i++) + { + int tryIndex = (index + i) & (map->capacity - 1); + if (!map->buckets[tryIndex].active) + { + map->buckets[tryIndex].key = key; + map->buckets[tryIndex].value = value; + map->buckets[tryIndex].active = true; + map->count++; + return true; + } + } + + // This should not be reached under normal circumstances + TRACELOG(LOG_WARNING, "FONT: Glyph lookup map is full, can not insert codepoint %d", key); + return false; +} + +static bool MapGet(GlyphLookupMap *map, int key, int *value) +{ + if (map == NULL || map->buckets == NULL) return false; + + unsigned int index = HashCodepoint(key) & (map->capacity - 1); + + for (int i = 0; i < map->capacity; i++) + { + int tryIndex = (index + i) & (map->capacity - 1); + if (map->buckets[tryIndex].active && map->buckets[tryIndex].key == key) + { + *value = map->buckets[tryIndex].value; + return true; + } + if (!map->buckets[tryIndex].active) break; // if it's empty we can exit earlier + } + + return false; +} + +static void GenFontGlyphLookupMap(Font *font) +{ + if (font == NULL || font->glyphs == NULL || font->glyphCount == 0) return; + + // make sure we have more than enough space without overallocating + // This must remain a power of two for CPU efficiency as the rest of the code uses bitwise operations on this value + int capacity = 16; + while (capacity < font->glyphCount * 1.5) capacity *= 2; + + GlyphLookupMap *map = CreateGlyphLookupMap(capacity); + if (map == NULL) + { + TRACELOG(LOG_WARNING, "FONT: Failed to create glyph lookup map"); + font->glyphLookup = NULL; + return; + } + + for (int i = 0; i < font->glyphCount; i++) + { + MapInsert(map, font->glyphs[i].value, i); + } + + font->glyphLookup = map; + TRACELOG(LOG_INFO, "FONT: Generated glyph lookup map for %d glyphs", font->glyphCount); +} + + #if defined(SUPPORT_FILEFORMAT_BDF) // Convert hexadecimal to decimal (single digit) static unsigned char HexToInt(char hex)