From 73581ffc5358d681bbea2522cdcf635883f4224c Mon Sep 17 00:00:00 2001 From: Jack Andersen Date: Sun, 22 Nov 2015 22:47:21 -1000 Subject: [PATCH] Initial font caching implementation --- specter/CMakeLists.txt | 5 +- specter/freetype2 | 2 +- specter/include/Specter/FontCache.hpp | 66 +++++- specter/lib/FontCache.cpp | 293 +++++++++++++++++++++++++- 4 files changed, 355 insertions(+), 11 deletions(-) diff --git a/specter/CMakeLists.txt b/specter/CMakeLists.txt index 359d2f9ad..daa565bc3 100644 --- a/specter/CMakeLists.txt +++ b/specter/CMakeLists.txt @@ -44,6 +44,8 @@ list(APPEND SPECTER_HEADERS include/Specter/NodeSocket.hpp include/Specter/FontCache.hpp) +atdna(atdna_FontCache.cpp include/Specter/FontCache.hpp) + list(APPEND SPECTER_SOURCES lib/Specter.cpp lib/View.cpp @@ -60,6 +62,7 @@ list(APPEND SPECTER_SOURCES lib/Menu.cpp lib/Node.cpp lib/NodeSocket.cpp - lib/FontCache.cpp) + lib/FontCache.cpp + atdna_FontCache.cpp) add_library(Specter ${SPECTER_SOURCES} ${SPECTER_HEADERS}) diff --git a/specter/freetype2 b/specter/freetype2 index bddbb5dd8..4998fba72 160000 --- a/specter/freetype2 +++ b/specter/freetype2 @@ -1 +1 @@ -Subproject commit bddbb5dd8a91591218ba2f50f4ef1c90ed8a4ca7 +Subproject commit 4998fba725b01da1a659ed09388ea4207ff03b02 diff --git a/specter/include/Specter/FontCache.hpp b/specter/include/Specter/FontCache.hpp index 8c94c6a8f..0b0bf91d4 100644 --- a/specter/include/Specter/FontCache.hpp +++ b/specter/include/Specter/FontCache.hpp @@ -6,6 +6,30 @@ #include #include +#include +#include + +namespace Specter +{ +class FontTag +{ + friend class FontCache; + uint64_t m_hash; + FontTag(const std::string& name, bool subpixel, float points, unsigned dpi); +public: + uint64_t hash() const {return m_hash;} + bool operator==(const FontTag& other) const {return m_hash == other.m_hash;} +}; +} + +namespace std +{ +template <> struct hash +{ + size_t operator() (const Specter::FontTag& handle) const NOEXCEPT + {return size_t(handle.hash());} +}; +} namespace Specter { @@ -21,19 +45,37 @@ public: operator FT_Face() {return m_face;} }; -class FontHandle -{ -}; - class FontAtlas { + friend class FontCache; FT_Face m_face; - std::vector m_texs; + boo::ITextureS* m_tex; + + struct Glyph + { + atUint32 m_unicodePoint; + atUint32 m_layerIdx; + float m_uv[4]; + atInt8 m_leftPadding; + atInt8 m_advance; + atInt8 m_rightPadding; + atUint8 m_width; + atUint8 m_height; + atInt8 m_verticalOffset; + atUint16 m_kernIdx; + }; + std::vector m_glyphs; + std::map m_glyphLookup; + +public: + FontAtlas(boo::IGraphicsDataFactory* gf, FT_Face face, bool subpixel, Athena::io::FileWriter& writer); + FontAtlas(boo::IGraphicsDataFactory* gf, FT_Face face, bool subpixel, Athena::io::FileReader& reader); }; class FontCache { const HECL::Runtime::FileStoreManager& m_fileMgr; + HECL::SystemString m_cacheRoot; struct Library { FT_Library m_lib; @@ -43,12 +85,20 @@ class FontCache } m_fontLib; FreeTypeGZipMemFace m_regFace; FreeTypeGZipMemFace m_monoFace; + + std::unordered_map> m_cachedAtlases; public: FontCache(const HECL::Runtime::FileStoreManager& fileMgr); - FontHandle prepMainFont(float points=10.0); - FontHandle prepMonoFont(float points=10.0); - FontHandle prepCustomFont(FT_Face face, float points=10.0); + FontTag prepCustomFont(boo::IGraphicsDataFactory* gf, + const std::string& name, FT_Face face, bool subpixel=false, + float points=10.0, uint32_t dpi=72); + FontTag prepMainFont(boo::IGraphicsDataFactory* gf, + bool subpixel=false, float points=10.0, uint32_t dpi=72) + {return prepCustomFont(gf, "droidsans-permissive", m_regFace, subpixel, points, dpi);} + FontTag prepMonoFont(boo::IGraphicsDataFactory* gf, + bool subpixel=false, float points=10.0, uint32_t dpi=72) + {return prepCustomFont(gf, "bmonofont", m_monoFace, subpixel, points, dpi);} }; } diff --git a/specter/lib/FontCache.cpp b/specter/lib/FontCache.cpp index ba3d92488..6745baf73 100644 --- a/specter/lib/FontCache.cpp +++ b/specter/lib/FontCache.cpp @@ -17,6 +17,17 @@ namespace Specter { static LogVisor::LogModule Log("Specter::FontCache"); +FontTag::FontTag(const std::string& name, bool subpixel, float points, uint32_t dpi) +{ + XXH64_state_t st; + XXH64_reset(&st, 0); + XXH64_update(&st, name.data(), name.size()); + XXH64_update(&st, &subpixel, 1); + XXH64_update(&st, &points, 4); + XXH64_update(&st, &dpi, 4); + m_hash = XXH64_digest(&st); +} + FreeTypeGZipMemFace::FreeTypeGZipMemFace(FT_Library lib, const uint8_t* data, size_t sz) { m_comp.base = (unsigned char*)data; @@ -43,6 +54,237 @@ FreeTypeGZipMemFace::~FreeTypeGZipMemFace() FT_Done_Face(m_face); } +#define TEXMAP_DIM 1024 + +static unsigned RoundUpPow2(unsigned v) +{ + v--; + v |= v >> 1; + v |= v >> 2; + v |= v >> 4; + v |= v >> 8; + v |= v >> 16; + v++; + return v; +} + +using GreyPixel = uint8_t; +static void MemcpyRect(GreyPixel* img, const FT_Bitmap* bmp, unsigned slice, unsigned x, unsigned y) +{ + unsigned sy = TEXMAP_DIM * slice + y; + for (unsigned i=0 ; irows ; ++i) + { + const unsigned char* s = &bmp->buffer[bmp->pitch*i]; + GreyPixel* t = &img[TEXMAP_DIM*sy+x]; + memcpy(t, s, bmp->width); + } +} + +union RgbaPixel +{ + uint8_t rgba[4]; + uint32_t pixel; +}; +static void MemcpyRect(RgbaPixel* img, const FT_Bitmap* bmp, unsigned slice, unsigned x, unsigned y) +{ + unsigned sy = TEXMAP_DIM * slice + y; + for (unsigned i=0 ; irows ; ++i) + { + const unsigned char* s = &bmp->buffer[bmp->pitch*i]; + RgbaPixel* t = &img[TEXMAP_DIM*sy+x]; + for (unsigned j=0 ; jwidth/3 ; ++j) + { + t[j].rgba[0] = s[j*3]; + t[j].rgba[1] = s[j*3+1]; + t[j].rgba[2] = s[j*3+2]; + t[j].rgba[3] = 0xff; + } + } +} + + +FontAtlas::FontAtlas(boo::IGraphicsDataFactory* gf, FT_Face face, bool subpixel, Athena::io::FileWriter& writer) +{ + FT_Int32 baseFlags = FT_LOAD_NO_BITMAP; + if (subpixel) + baseFlags |= FT_LOAD_TARGET_LCD; + else + baseFlags |= FT_LOAD_TARGET_NORMAL; + + /* First count glyphs exposed by unicode charmap and tally required area */ + size_t glyphCount = 0; + FT_UInt gindex; + FT_ULong charcode = FT_Get_First_Char(face, &gindex); + unsigned curLineWidth = 0; + unsigned curLineHeight = 0; + unsigned totalHeight = 0; + unsigned fullTexmap = 0; + while (gindex != 0) + { + ++glyphCount; + FT_Load_Glyph(face, gindex, baseFlags); + unsigned width = face->glyph->bitmap.width; + if (subpixel) + width /= 3; + if (curLineWidth + width > TEXMAP_DIM) + { + if (totalHeight + curLineHeight > TEXMAP_DIM) + { + totalHeight = 0; + ++fullTexmap; + } + else + totalHeight += curLineHeight; + curLineHeight = 0; + curLineWidth = 0; + } + curLineHeight = std::max(curLineHeight, face->glyph->bitmap.rows); + charcode = FT_Get_Next_Char(face, charcode, &gindex); + } + m_glyphs.reserve(glyphCount); + + totalHeight = RoundUpPow2(totalHeight); + unsigned finalHeight = fullTexmap ? TEXMAP_DIM : totalHeight; + writer.writeUint32Big(fullTexmap); + writer.writeUint32Big(TEXMAP_DIM); + writer.writeUint32Big(finalHeight); + + if (subpixel) + { + /* Allocate texmap */ + std::unique_ptr texmap; + size_t bufSz; + if (fullTexmap) + { + size_t count = TEXMAP_DIM * TEXMAP_DIM * (fullTexmap + 1); + texmap.reset(new RgbaPixel[count]); + bufSz = count * sizeof(RgbaPixel); + memset(texmap.get(), 0, bufSz); + } + else + { + size_t count = TEXMAP_DIM * totalHeight; + texmap.reset(new RgbaPixel[TEXMAP_DIM * totalHeight]); + bufSz = count * sizeof(RgbaPixel); + memset(texmap.get(), 0, bufSz); + } + + /* Assemble glyph texmaps and internal data structures */ + charcode = FT_Get_First_Char(face, &gindex); + curLineWidth = 0; + curLineHeight = 0; + totalHeight = 0; + fullTexmap = 0; + while (gindex != 0) + { + FT_Load_Glyph(face, gindex, FT_LOAD_RENDER | baseFlags); + m_glyphs.emplace_back(); + Glyph& g = m_glyphs.back(); + g.m_unicodePoint = charcode; + g.m_layerIdx = fullTexmap; + g.m_width = face->glyph->bitmap.width / 3; + g.m_height = face->glyph->bitmap.rows; + g.m_uv[0] = curLineWidth / float(TEXMAP_DIM); + g.m_uv[1] = totalHeight / float(finalHeight); + g.m_uv[2] = g.m_uv[0] + g.m_width / float(TEXMAP_DIM); + g.m_uv[3] = g.m_uv[1] + g.m_height / float(finalHeight); + g.m_leftPadding = 0; + g.m_advance = face->glyph->advance.x; + g.m_rightPadding = 0; + g.m_verticalOffset = face->glyph->metrics.horiBearingY; + g.m_kernIdx = 0; + MemcpyRect(texmap.get(), &face->glyph->bitmap, fullTexmap, curLineWidth, totalHeight); + if (curLineWidth + g.m_width > TEXMAP_DIM) + { + if (totalHeight + curLineHeight > TEXMAP_DIM) + { + totalHeight = 0; + ++fullTexmap; + } + else + totalHeight += curLineHeight; + curLineHeight = 0; + curLineWidth = 0; + } + curLineHeight = std::max(curLineHeight, face->glyph->bitmap.rows); + charcode = FT_Get_Next_Char(face, charcode, &gindex); + } + + m_tex = + gf->newStaticArrayTexture(TEXMAP_DIM, finalHeight, fullTexmap + 1, + boo::TextureFormat::RGBA8, texmap.get(), bufSz); + } + else + { + /* Allocate texmap */ + std::unique_ptr texmap; + size_t bufSz; + if (fullTexmap) + { + size_t count = TEXMAP_DIM * TEXMAP_DIM * (fullTexmap + 1); + texmap.reset(new GreyPixel[count]); + bufSz = count * sizeof(GreyPixel); + memset(texmap.get(), 0, bufSz); + } + else + { + size_t count = TEXMAP_DIM * totalHeight; + texmap.reset(new GreyPixel[TEXMAP_DIM * totalHeight]); + bufSz = count * sizeof(GreyPixel); + memset(texmap.get(), 0, bufSz); + } + + /* Assemble glyph texmaps and internal data structures */ + charcode = FT_Get_First_Char(face, &gindex); + curLineWidth = 0; + curLineHeight = 0; + totalHeight = 0; + fullTexmap = 0; + while (gindex != 0) + { + FT_Load_Glyph(face, gindex, FT_LOAD_RENDER | baseFlags); + m_glyphs.emplace_back(); + Glyph& g = m_glyphs.back(); + g.m_unicodePoint = charcode; + g.m_layerIdx = fullTexmap; + g.m_width = face->glyph->bitmap.width; + g.m_height = face->glyph->bitmap.rows; + g.m_uv[0] = curLineWidth / float(TEXMAP_DIM); + g.m_uv[1] = totalHeight / float(finalHeight); + g.m_uv[2] = g.m_uv[0] + g.m_width / float(TEXMAP_DIM); + g.m_uv[3] = g.m_uv[1] + g.m_height / float(finalHeight); + g.m_leftPadding = 0; + g.m_advance = face->glyph->advance.x; + g.m_rightPadding = 0; + g.m_verticalOffset = face->glyph->metrics.horiBearingY; + g.m_kernIdx = 0; + MemcpyRect(texmap.get(), &face->glyph->bitmap, fullTexmap, curLineWidth, totalHeight); + if (curLineWidth + g.m_width > TEXMAP_DIM) + { + if (totalHeight + curLineHeight > TEXMAP_DIM) + { + totalHeight = 0; + ++fullTexmap; + } + else + totalHeight += curLineHeight; + curLineHeight = 0; + curLineWidth = 0; + } + curLineHeight = std::max(curLineHeight, face->glyph->bitmap.rows); + charcode = FT_Get_Next_Char(face, charcode, &gindex); + } + + m_tex = + gf->newStaticArrayTexture(TEXMAP_DIM, finalHeight, fullTexmap + 1, + boo::TextureFormat::I8, texmap.get(), bufSz); + } +} + +FontAtlas::FontAtlas(boo::IGraphicsDataFactory* gf, FT_Face face, bool subpixel, Athena::io::FileReader& reader) +{ +} + FontCache::Library::Library() { FT_Error err = FT_Init_FreeType(&m_lib); @@ -66,8 +308,57 @@ static FT_Library InitLib() FontCache::FontCache(const HECL::Runtime::FileStoreManager& fileMgr) : m_fileMgr(fileMgr), + m_cacheRoot(m_fileMgr.getStoreRoot() + _S("/fontcache")), m_regFace(m_fontLib, DROIDSANS_PERMISSIVE, DROIDSANS_PERMISSIVE_SZ), - m_monoFace(m_fontLib, BMONOFONT, BMONOFONT_SZ) {} + m_monoFace(m_fontLib, BMONOFONT, BMONOFONT_SZ) +{HECL::MakeDir(m_cacheRoot.c_str());} +FontTag FontCache::prepCustomFont(boo::IGraphicsDataFactory* gf, + const std::string& name, FT_Face face, bool subpixel, + float points, uint32_t dpi) +{ + /* Quick validation */ + if (!face) + Log.report(LogVisor::FatalError, "invalid freetype face"); + + if (!face->charmap || face->charmap->encoding != ft_encoding_unicode) + Log.report(LogVisor::FatalError, "font does not contain a unicode char map"); + + /* Set size with FreeType */ + FT_Set_Char_Size(face, points * 64.0, 0, dpi, 0); + + /* Make tag and search for cached version */ + FontTag tag(name, subpixel, points, dpi); + auto search = m_cachedAtlases.find(tag); + if (search != m_cachedAtlases.end()) + return tag; + + /* Now check filesystem cache */ + HECL::SystemString cachePath = m_cacheRoot + _S('/') + HECL::Format("%" PRIx64, tag.hash()); +#if 0 + HECL::Sstat st; + if (!HECL::Stat(cachePath.c_str(), &st) && S_ISREG(st.st_mode)) + { + Athena::io::FileReader r(cachePath); + if (!r.hasError()) + { + atUint32 magic = r.readUint32Big(); + if (r.position() == 4 && magic == 'FONT') + { + m_cachedAtlases.emplace(tag, std::make_unique(gf, face, subpixel, r)); + return tag; + } + } + } +#endif + + /* Nada, build and cache now */ + Athena::io::FileWriter w(cachePath); + if (w.hasError()) + Log.report(LogVisor::FatalError, "unable to open '%s' for writing", cachePath.c_str()); + w.writeUint32Big('FONT'); + m_cachedAtlases.emplace(tag, std::make_unique(gf, face, subpixel, w)); + return tag; +} }