// Copyright (c) Charles J. Cliffe // SPDX-License-Identifier: GPL-2.0+ #include "GLFont.h" #include #include #include #include #include "cubic_math.h" #ifdef _OSX_APP_ #include "CoreFoundation/CoreFoundation.h" #endif #ifndef RES_FOLDER #define RES_FOLDER L"" #endif #define GC_DRAW_COUNT_PERIOD 50 #define GC_DRAW_COUNT_LIMIT 10 GLFontStringCache::GLFontStringCache() { gc = 0; } //Static initialization of all available fonts, //using aggregate syntax (Cx11+) //Fonts must be listed in increasing size for Drawer to work ! GLFont GLFont::fonts[GLFont::GLFontSize::GLFONT_SIZE_MAX] = { { GLFont::GLFontSize::GLFONT_SIZE12, L"fonts/vera_sans_mono12.fnt" }, { GLFont::GLFontSize::GLFONT_SIZE16, L"fonts/vera_sans_mono16.fnt" }, { GLFont::GLFontSize::GLFONT_SIZE18, L"fonts/vera_sans_mono18.fnt" }, { GLFont::GLFontSize::GLFONT_SIZE24, L"fonts/vera_sans_mono24.fnt" }, { GLFont::GLFontSize::GLFONT_SIZE27, L"fonts/vera_sans_mono27.fnt" }, { GLFont::GLFontSize::GLFONT_SIZE32, L"fonts/vera_sans_mono32.fnt" }, { GLFont::GLFontSize::GLFONT_SIZE36, L"fonts/vera_sans_mono36.fnt" }, { GLFont::GLFontSize::GLFONT_SIZE48, L"fonts/vera_sans_mono48.fnt" }, { GLFont::GLFontSize::GLFONT_SIZE64, L"fonts/vera_sans_mono64.fnt" }, { GLFont::GLFontSize::GLFONT_SIZE72, L"fonts/vera_sans_mono72.fnt" }, { GLFont::GLFontSize::GLFONT_SIZE96, L"fonts/vera_sans_mono96.fnt" }, }; std::atomic GLFont::currentScale{ GLFont::GLFontScale::GLFONT_SCALE_NORMAL }; GLFontChar::GLFontChar() : id(0), x(0), y(0), width(0), height(0), xoffset(0), yoffset(0), xadvance(0), aspect(1), index(0) { } GLFontChar::~GLFontChar() = default; void GLFontChar::setId(int idval) { id = idval; } int GLFontChar::getId() const { return id; } void GLFontChar::setXOffset(int xofs) { xoffset = xofs; } int GLFontChar::getXOffset() { return xoffset; } void GLFontChar::setYOffset(int yofs) { yoffset = yofs; } int GLFontChar::getYOffset() { return yoffset; } void GLFontChar::setX(int xpos) { x = xpos; } int GLFontChar::getX() { return x; } void GLFontChar::setY(int ypos) { y = ypos; } int GLFontChar::getY() { return y; } void GLFontChar::setWidth(int w) { width = w; if (width && height) { aspect = (float) width / (float) height; } } int GLFontChar::getWidth() { return width; } void GLFontChar::setHeight(int h) { height = h; if (width && height) { aspect = (float) width / (float) height; } } int GLFontChar::getHeight() const { return height; } void GLFontChar::setXAdvance(int xadv) { xadvance = xadv; } int GLFontChar::getXAdvance() { return xadvance; } float GLFontChar::getAspect() const { return aspect; } void GLFontChar::setIndex(unsigned int idx) { index = idx; } int GLFontChar::getIndex() const { return index; } GLFont::GLFont(GLFontSize size, std::wstring defFileName): lineHeight(0), base(0), imageWidth(0), imageHeight(0), loaded(false), texId(0), gcCounter(0) { fontSizeClass = size; fontDefFileSource = defFileName; } GLFont::~GLFont() = default; std::wstring GLFont::nextParam(std::wistringstream &str) { std::wstring param_str; str >> param_str; if (param_str.find(L'"') != std::wstring::npos) { std::wstring rest; while (!str.eof() && (std::count(param_str.begin(), param_str.end(), L'"') % 2)) { str >> rest; param_str.append(L" " + rest); } } return param_str; } std::wstring GLFont::getParamKey(const std::wstring& param_str) { std::wstring keyName; size_t eqpos = param_str.find(L'='); if (eqpos != std::wstring::npos) { keyName = param_str.substr(0, eqpos); } return keyName; } std::wstring GLFont::getParamValue(const std::wstring& param_str) { std::wstring value; size_t eqpos = param_str.find(L'='); if (eqpos != std::wstring::npos) { value = param_str.substr(eqpos + 1); } if (value[0] == L'"' && value[value.length() - 1] == L'"') { value = value.substr(1, value.length() - 2); } return value; } void GLFont::loadFontOnce() { if (loaded) { return; } #if _OSX_APP_ CFBundleRef mainBundle = CFBundleGetMainBundle(); CFURLRef resourcesURL = CFBundleCopyResourcesDirectoryURL(mainBundle); char path[PATH_MAX]; if (!CFURLGetFileSystemRepresentation(resourcesURL, TRUE, (UInt8 *)path, PATH_MAX)) { // error! } CFRelease(resourcesURL); wxString resourceFolder = std::string(path) + "/"; #else wxString resourceFolder = RES_FOLDER; #endif wxFileName exePath = wxFileName(wxStandardPaths::Get().GetExecutablePath()); //1) First try : RES_FOLDER/fonts/* wxFileName fontDefFileName = wxFileName(resourceFolder + L"/" + fontDefFileSource); bool fontFilePathFound = fontDefFileName.Exists(); // 2) Second try: [Cubic exe path]/RES_FOLDER/fonts/* if (!fontFilePathFound) { fontDefFileName = wxFileName(exePath.GetPath() + L"/" + RES_FOLDER + L"/" + fontDefFileSource); fontFilePathFound = fontDefFileName.Exists(); } // 3) Third try: [Cubic exe path]/fonts/* if (!fontFilePathFound) { fontDefFileName = wxFileName(exePath.GetPath() + L"/" + fontDefFileSource); fontFilePathFound = fontDefFileName.Exists(); } if (fontFilePathFound) { if (!fontDefFileName.IsFileReadable()) { std::cout << "Font file " << fontDefFileName.GetFullPath() << " is not readable?" << std::endl; return; } } else { if (!fontDefFileName.FileExists()) { std::cout << "Font file " << fontDefFileName.GetFullPath() << " does not exist?" << std::endl; return; } } //Re-compute the resource dir. resourceFolder = fontDefFileName.GetPath(); std::string fontDefFileNamePath = fontDefFileName.GetFullPath(wxPATH_NATIVE).ToStdString(); std::wifstream input; input.open(fontDefFileNamePath, std::ios::in); std::wstring op; while (!input.eof()) { input >> op; if (op == L"info") { std::wstring info_param_str; getline(input, info_param_str); std::wistringstream info_param(info_param_str); while (!info_param.eof()) { std::wstring param = nextParam(info_param); std::wstring paramKey = getParamKey(param); if (paramKey == L"face") { fontName = getParamValue(param); } param = nextParam(info_param); paramKey = getParamKey(param); if (paramKey == L"size") { std::wistringstream paramValue(getParamValue(param)); paramValue >> pixHeight; } // std::cout << "[" << paramKey << "] = '" << paramValue << "'" << std::endl; } } else if (op == L"common") { std::wstring common_param_str; getline(input, common_param_str); std::wistringstream common_param(common_param_str); while (!common_param.eof()) { std::wstring param = nextParam(common_param); std::wstring paramKey = getParamKey(param); std::wistringstream paramValue(getParamValue(param)); if (paramKey == L"lineHeight") { paramValue >> lineHeight; } else if (paramKey == L"base") { paramValue >> base; } else if (paramKey == L"scaleW") { paramValue >> imageWidth; } else if (paramKey == L"scaleH") { paramValue >> imageHeight; } // std::cout << "[" << paramKey << "] = '" << getParamValue(param) << "'" << std::endl; } } else if (op == L"page") { std::wstring page_param_str; getline(input, page_param_str); std::wistringstream page_param(page_param_str); while (!page_param.eof()) { std::wstring param = nextParam(page_param); std::wstring paramKey = getParamKey(param); std::wstring paramValue = getParamValue(param); if (paramKey == L"file") { wxFileName imgFileName = wxFileName(resourceFolder, paramValue); imageFile = imgFileName.GetFullPath(wxPATH_NATIVE).ToStdWstring(); } // std::cout << "[" << paramKey << "] = '" << paramValue << "'" << std::endl; } } else if (op == L"char") { std::wstring char_param_str; getline(input, char_param_str); std::wistringstream char_param(char_param_str); auto *newChar = new GLFontChar; while (!char_param.eof()) { std::wstring param = nextParam(char_param); std::wstring paramKey = getParamKey(param); std::wistringstream paramValue(getParamValue(param)); int val; if (paramKey == L"id") { paramValue >> val; newChar->setId(val); } else if (paramKey == L"x") { paramValue >> val; newChar->setX(val); } else if (paramKey == L"y") { paramValue >> val; newChar->setY(val); } else if (paramKey == L"width") { paramValue >> val; newChar->setWidth(val); } else if (paramKey == L"height") { paramValue >> val; newChar->setHeight(val); } else if (paramKey == L"xoffset") { paramValue >> val; newChar->setXOffset(val); } else if (paramKey == L"yoffset") { paramValue >> val; newChar->setYOffset(val); } else if (paramKey == L"xadvance") { paramValue >> val; newChar->setXAdvance(val); } // std::cout << "[" << paramKey << "] = '" << getParamValue(param) << "'" << std::endl; } characters[newChar->getId()] = newChar; } else { std::wstring dummy; getline(input, dummy); } } if (!imageFile.empty() && imageWidth && imageHeight && !characters.empty()) { // Load file and decode image. std::vector image; unsigned int imgWidth = imageWidth, imgHeight = imageHeight; //1) First load the raw file to memory using wstring filenames wxFile png_file(imageFile); int png_size = png_file.Length(); auto* raw_image = new unsigned char[png_size]; if (png_size > 0) { int nbRead = png_file.Read((void*)raw_image, png_size); if (png_size != nbRead) { std::cout << "Error loading the full PNG image file in memory: '" << imageFile << "'" << std::endl; } } //2) then load from memory unsigned error = lodepng::decode(image, imgWidth, imgHeight, raw_image, png_size); delete[] raw_image; png_file.Close(); if (error) { std::cout << "Error decoding PNG image file: '" << imageFile << "'" << std::endl; } glGenTextures(1, &texId); glEnable(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, texId); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexImage2D(GL_TEXTURE_2D, 0, 4, imageWidth, imageHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, error?nullptr:(&image[0])); glDisable(GL_TEXTURE_2D); std::map::iterator char_i; gl_vertices.resize(characters.size() * 8); // one quad per char gl_uv.resize(characters.size() * 8); unsigned int ofs = 0; for (char_i = characters.begin(); char_i != characters.end(); char_i++) { // int charId = (*char_i).first; GLFontChar *fchar = (*char_i).second; float faspect = fchar->getAspect(); float uv_xpos = (float) fchar->getX() / (float) imageWidth; float uv_ypos = ((float) fchar->getY() / (float) imageHeight); float uv_xofs = (float) fchar->getWidth() / (float) imageWidth; float uv_yofs = ((float) fchar->getHeight() / (float) imageHeight); gl_vertices[ofs] = 0; gl_vertices[ofs + 1] = 0; gl_uv[ofs] = uv_xpos; gl_uv[ofs + 1] = uv_ypos + uv_yofs; gl_vertices[ofs + 2] = faspect; gl_vertices[ofs + 3] = 0; gl_uv[ofs + 2] = uv_xpos + uv_xofs; gl_uv[ofs + 3] = uv_ypos + uv_yofs; gl_vertices[ofs + 4] = faspect; gl_vertices[ofs + 5] = 1; gl_uv[ofs + 4] = uv_xpos + uv_xofs; gl_uv[ofs + 5] = uv_ypos; gl_vertices[ofs + 6] = 0; gl_vertices[ofs + 7] = 1; gl_uv[ofs + 6] = uv_xpos; gl_uv[ofs + 7] = uv_ypos; fchar->setIndex(ofs); ofs += 8; } std::cout << "Loaded font '" << fontName << "' from '" << imageFile << "', parsed " << characters.size() << " characters." << std::endl; loaded = true; } else { std::cout << "Error loading font file " << imageFile << std::endl; } input.close(); loaded = true; } float GLFont::getStringWidth(const std::wstring& str, float size, float viewAspect) { float scalex = size / viewAspect; float width = 0; for (int charId : str) { if (characters.find(charId) == characters.end()) { continue; } GLFontChar *fchar = characters[charId]; float ofsx = (float) fchar->getXOffset() / (float) imageWidth; float advx = (float) fchar->getXAdvance() / (float) imageWidth; if (charId == 32) { advx = characters[L'_']->getAspect(); } width += fchar->getAspect() + advx + ofsx; } width *= scalex; return width; } // Draw string, immediate void GLFont::drawString(const std::wstring& str, int pxHeight, float xpos, float ypos, Align hAlign, Align vAlign, int vpx, int vpy, bool cacheable) { pxHeight *= 2; if (!vpx || !vpy) { GLint vp[4]; glGetIntegerv( GL_VIEWPORT, vp); vpx = vp[2]; vpy = vp[3]; } if (cacheable) { gcCounter++; std::lock_guard lock(cache_busy); if (gcCounter > GC_DRAW_COUNT_PERIOD) { doCacheGC(); gcCounter = 0; } GLFontStringCache *fc = nullptr; std::map::iterator cache_iter; std::wstringstream sscacheIdx; sscacheIdx << vpx << "." << vpy << "." << pxHeight << "." << str; std::wstring cacheIdx(sscacheIdx.str()); cache_iter = stringCache.find(cacheIdx); if (cache_iter != stringCache.end()) { fc = cache_iter->second; fc->gc = 0; } if (fc == nullptr) { // std::cout << "cache miss" << std::endl; fc = cacheString(str, pxHeight, vpx, vpy); stringCache[cacheIdx] = fc; } drawCacheString(fc, xpos, ypos, hAlign, vAlign); return; } float size = (float) pxHeight / (float) vpy; float viewAspect = (float) vpx / (float) vpy; float msgWidth = getStringWidth(str, size, viewAspect); glPushMatrix(); glTranslatef(xpos, ypos, 0.0f); switch (vAlign) { case GLFONT_ALIGN_TOP: glTranslatef(0.0, -size, 0.0); break; case GLFONT_ALIGN_CENTER: glTranslatef(0.0, -size/2.0, 0.0); break; default: break; } switch (hAlign) { case GLFONT_ALIGN_RIGHT: glTranslatef(-msgWidth, 0.0, 0.0); break; case GLFONT_ALIGN_CENTER: glTranslatef(-msgWidth / 2.0, 0.0, 0.0); break; default: break; } glPushMatrix(); glScalef(size / viewAspect, size, 1.0f); glEnable(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, texId); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glEnableClientState(GL_VERTEX_ARRAY); glEnableClientState(GL_TEXTURE_COORD_ARRAY); glVertexPointer(2, GL_FLOAT, 0, &gl_vertices[0]); glTexCoordPointer(2, GL_FLOAT, 0, &gl_uv[0]); for (int charId : str) { if (characters.find(charId) == characters.end()) { continue; } GLFontChar *fchar = characters[charId]; float ofsx = (float) fchar->getXOffset() / (float) imageWidth; float advx = (float) fchar->getXAdvance() / (float) imageWidth; if (charId == 32) { advx = characters[L'_']->getAspect(); } glTranslatef(ofsx, 0.0, 0.0); glDrawArrays(GL_QUADS, fchar->getIndex() / 2, 4); glTranslatef(fchar->getAspect() + advx, 0.0, 0.0); } glVertexPointer(2, GL_FLOAT, 0, nullptr); glTexCoordPointer(2, GL_FLOAT, 0, nullptr); glDisableClientState(GL_VERTEX_ARRAY); glDisableClientState(GL_TEXTURE_COORD_ARRAY); glPopMatrix(); glPopMatrix(); glDisable(GL_BLEND); glDisable(GL_TEXTURE_2D); } // Draw string, immediate, 8 bit version void GLFont::drawString(const std::string& str, int pxHeight, float xpos, float ypos, Align hAlign, Align vAlign, int vpx, int vpy, bool cacheable) { //Displayed string is wstring, so use wxString to do the heavy lifting of converting str... wxString wsTmp; wsTmp.assign(str); drawString(wsTmp.ToStdWstring(), pxHeight, xpos, ypos, hAlign, vAlign, vpx, vpy, cacheable); } // Draw cached GLFontCacheString void GLFont::drawCacheString(GLFontStringCache *fc, float xpos, float ypos, Align hAlign, Align vAlign) const { float size = (float) fc->pxHeight / (float) fc->vpy; glPushMatrix(); glTranslatef(xpos, ypos, 0.0f); switch (vAlign) { case GLFONT_ALIGN_TOP: glTranslatef(0.0, -size, 0.0); break; case GLFONT_ALIGN_CENTER: glTranslatef(0.0, -size/2.0, 0.0); break; default: break; } switch (hAlign) { case GLFONT_ALIGN_RIGHT: glTranslatef(-fc->msgWidth, 0.0, 0.0); break; case GLFONT_ALIGN_CENTER: glTranslatef(-fc->msgWidth / 2.0, 0.0, 0.0); break; default: break; } glEnable(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, texId); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glEnableClientState(GL_VERTEX_ARRAY); glEnableClientState(GL_TEXTURE_COORD_ARRAY); glVertexPointer(2, GL_FLOAT, 0, &fc->gl_vertices[0]); glTexCoordPointer(2, GL_FLOAT, 0, &fc->gl_uv[0]); glDrawArrays(GL_QUADS, 0, 4 * fc->drawlen); glVertexPointer(2, GL_FLOAT, 0, nullptr); glTexCoordPointer(2, GL_FLOAT, 0, nullptr); glDisableClientState(GL_VERTEX_ARRAY); glDisableClientState(GL_TEXTURE_COORD_ARRAY); glPopMatrix(); glDisable(GL_BLEND); glDisable(GL_TEXTURE_2D); } // Compile optimized GLFontCacheString GLFontStringCache *GLFont::cacheString(const std::wstring& str, int pxHeight, int vpx, int vpy) { auto *fc = new GLFontStringCache; fc->pxHeight = pxHeight; fc->vpx = vpx; fc->vpy = vpy; float size = (float) pxHeight / (float) vpy; float viewAspect = (float) vpx / (float) vpy; fc->msgWidth = getStringWidth(str, size, viewAspect); int nChar = 0; for (int charId : str) { if (characters.find(charId) == characters.end()) { continue; } nChar++; } fc->drawlen = nChar; fc->gl_vertices.resize(nChar*8); fc->gl_uv.resize(nChar*8); CubicVR::mat4 trans = CubicVR::mat4::scale(size / viewAspect, size, 1.0f); int c = 0; for (int charId : str) { if (characters.find(charId) == characters.end()) { continue; } GLFontChar *fchar = characters[charId]; float ofsx = (float) fchar->getXOffset() / (float) imageWidth; float advx = (float) fchar->getXAdvance() / (float) imageWidth; if (charId == 32) { advx = characters[L'_']->getAspect(); } // freeze transform to buffer trans *= CubicVR::mat4::translate(ofsx, 0.0, 0.0); int charIdx = fchar->getIndex(); for (int j = 0; j < 8; j+=2) { CubicVR::vec3 pt(gl_vertices[charIdx + j],gl_vertices[charIdx + j + 1], 0.0); pt = CubicVR::mat4::multiply(trans, pt, true); fc->gl_vertices[c * 8 + j] = pt[0]; fc->gl_vertices[c * 8 + j + 1] = pt[1]; fc->gl_uv[c * 8 + j] = gl_uv[charIdx + j]; fc->gl_uv[c * 8 + j + 1] = gl_uv[charIdx + j + 1]; } trans *= CubicVR::mat4::translate(fchar->getAspect() + advx, 0.0, 0.0); c++; } return fc; } void GLFont::doCacheGC() { std::map::iterator cache_iter; bool flushDone = false; //do aging and remove in one pass. cache_iter = stringCache.begin(); while (cache_iter != stringCache.end()) { //aging cache_iter->second->gc--; //only flush 1 element per call if (!flushDone && cache_iter->second->gc < -GC_DRAW_COUNT_LIMIT) { delete cache_iter->second; cache_iter = stringCache.erase(cache_iter); flushDone = true; } else { cache_iter++; } } //end while } void GLFont::clearCache() { std::lock_guard lock(cache_busy); std::map::iterator cache_iter; cache_iter = stringCache.begin(); while (cache_iter != stringCache.end()) { delete cache_iter->second; cache_iter = stringCache.erase(cache_iter); } } void GLFont::clearAllCaches() { for (int i = 0; i < GLFont::GLFONT_SIZE_MAX; i++) { fonts[i].clearCache(); } } GLFont::Drawer GLFont::getFont(int requestedSize, double scaleFactor) { return GLFont::Drawer(requestedSize, scaleFactor); } void GLFont::setScale(GLFontScale scale) { //safety vs. inputs if (scale < GLFONT_SCALE_NORMAL || scale > GLFONT_SCALE_LARGE) { scale = GLFontScale::GLFONT_SCALE_NORMAL; } currentScale.store(scale); //Flush all the GC stuff clearAllCaches(); } GLFont::GLFontScale GLFont::getScale() { return currentScale.load(); } double GLFont::getScaleFactor() { GLFontScale scale = currentScale.load(); if (scale == GLFONT_SCALE_MEDIUM) { return 1.5; } else if (scale == GLFONT_SCALE_LARGE) { return 2.0; } return 1.0; } int GLFont::getScaledPx(int basicFontSize, double scaleFactor) { //try to align on an integer pixel size if the targetSize font is available int targetSize = round(basicFontSize * scaleFactor); int resultIndex = 0; fonts[0].loadFontOnce(); for (int i = 0; i < GLFONT_SIZE_MAX - 1; i++) { fonts[i + 1].loadFontOnce(); if (fonts[i + 1].pixHeight <= targetSize) { resultIndex = i + 1; } else { break; } } //end for // return font height px return fonts[resultIndex].pixHeight; } GLFont::Drawer::Drawer(int basicFontSize, double scaleFactor) { //Selection of the final font: scan GLFont::fonts to find the biggest font such as // its pixHeight <= basicFontSize * scaleFactor. //then compute finalScaleFactor the zooming factor of renderingFont to reach a //final font size of basicFontSize* scaleFactor: renderingFontIndex = 0; //try to align on an integer pixel size if the targetSize font is available int targetSize = round(basicFontSize * scaleFactor); fonts[0].loadFontOnce(); for (int i = 0; i < GLFONT_SIZE_MAX - 1; i++) { fonts[i + 1].loadFontOnce(); if (fonts[i + 1].pixHeight <= targetSize) { renderingFontIndex = i + 1; } else { break; } } //end for // int rawSize = fonts[renderingFontIndex].pixHeight; //targetSize may not be reached yet, so the effective rendering font: fonts[renderingFontIndex] must be scaled up a bit. renderingFontScaleFactor = (double) targetSize / rawSize; } void GLFont::Drawer::drawString(const std::wstring& str, float xpos, float ypos, Align hAlign, Align vAlign, int vpx, int vpy, bool cacheable) const { GLFont& appliedFont = fonts[renderingFontIndex]; appliedFont.drawString(str, round(appliedFont.pixHeight * renderingFontScaleFactor), xpos, ypos, hAlign, vAlign, vpx, vpy, cacheable); } //Public drawing font, 8 bit char version. void GLFont::Drawer::drawString(const std::string& str, float xpos, float ypos, Align hAlign, Align vAlign, int vpx, int vpy, bool cacheable) const { GLFont& appliedFont = fonts[renderingFontIndex]; appliedFont.drawString(str, round(appliedFont.pixHeight * renderingFontScaleFactor), xpos, ypos, hAlign, vAlign, vpx, vpy, cacheable); }