/** * Copyright (c) 2006-2015 LOVE Development Team * * This software is provided 'as-is', without any express or implied * warranty. In no event will the authors be held liable for any damages * arising from the use of this software. * * Permission is granted to anyone to use this software for any purpose, * including commercial applications, and to alter it and redistribute it * freely, subject to the following restrictions: * * 1. The origin of this software must not be misrepresented; you must not * claim that you wrote the original software. If you use this software * in a product, an acknowledgment in the product documentation would be * appreciated but is not required. * 2. Altered source versions must be plainly marked as such, and must not be * misrepresented as being the original software. * 3. This notice may not be removed or altered from any source distribution. **/ #include "common/config.h" #include "Font.h" #include "font/GlyphData.h" #include "libraries/utf8/utf8.h" #include "common/math.h" #include "common/Matrix.h" #include #include #include // for max #include namespace love { namespace graphics { namespace opengl { int Font::fontCount = 0; Font::Font(love::font::Rasterizer *r, const Texture::Filter &filter) : rasterizers({r}) , height(r->getHeight()) , lineHeight(1) , textureWidth(128) , textureHeight(128) , filter(filter) , useSpacesAsTab(false) , quadIndices(20) // We make this bigger at draw-time, if needed. , textureCacheID(0) , textureMemorySize(0) { this->filter.mipmap = Texture::FILTER_NONE; // Try to find the best texture size match for the font size. default to the // largest texture size if no rough match is found. while (true) { if ((height * 0.8) * height * 30 <= textureWidth * textureHeight) break; TextureSize nextsize = getNextTextureSize(); if (nextsize.width <= textureWidth && nextsize.height <= textureHeight) break; textureWidth = nextsize.width; textureHeight = nextsize.height; } love::font::GlyphData *gd = r->getGlyphData(32); // Space character. type = (gd->getFormat() == font::GlyphData::FORMAT_LUMINANCE_ALPHA) ? FONT_TRUETYPE : FONT_IMAGE; gd->release(); if (!r->hasGlyph(9)) // No tab character in the Rasterizer. useSpacesAsTab = true; loadVolatile(); ++fontCount; } Font::~Font() { unloadVolatile(); --fontCount; } Font::TextureSize Font::getNextTextureSize() const { TextureSize size = {textureWidth, textureHeight}; int maxsize = std::min(4096, gl.getMaxTextureSize()); if (size.width * 2 <= maxsize || size.height * 2 <= maxsize) { // {128, 128} -> {256, 128} -> {256, 256} -> {512, 256} -> etc. if (size.width == size.height) size.width *= 2; else size.height *= 2; } return size; } void Font::createTexture() { OpenGL::TempDebugGroup debuggroup("Font create texture"); GLenum format = type == FONT_TRUETYPE ? GL_LUMINANCE_ALPHA : GL_RGBA; size_t bpp = format == GL_LUMINANCE_ALPHA ? 2 : 4; size_t prevmemsize = textureMemorySize; if (prevmemsize > 0) { textureMemorySize -= (textureWidth * textureHeight * bpp); gl.updateTextureMemorySize(prevmemsize, textureMemorySize); } GLuint t = 0; TextureSize size = {textureWidth, textureHeight}; TextureSize nextsize = getNextTextureSize(); bool recreatetexture = false; // If we have an existing texture already, we'll try replacing it with a // larger-sized one rather than creating a second one. Having a single // texture reduces texture switches and draw calls when rendering. if ((nextsize.width > size.width || nextsize.height > size.height) && !textures.empty()) { recreatetexture = true; size = nextsize; t = textures.back(); } else glGenTextures(1, &t); gl.bindTexture(t); gl.setTextureFilter(filter); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); GLenum internalformat = type == FONT_TRUETYPE ? GL_LUMINANCE8_ALPHA8 : GL_RGBA8; // In GLES2, the internalformat and format params of TexImage have to match. // There is still no GL_LUMINANCE8_ALPHA8 in GLES3, so we have to use // GL_LUMINANCE_ALPHA even on ES3. if (GLAD_ES_VERSION_2_0) internalformat = format; // Initialize the texture with transparent black. std::vector emptydata(size.width * size.height * bpp, 0); // Clear errors before initializing. while (glGetError() != GL_NO_ERROR); glTexImage2D(GL_TEXTURE_2D, 0, internalformat, size.width, size.height, 0, format, GL_UNSIGNED_BYTE, &emptydata[0]); if (glGetError() != GL_NO_ERROR) { if (!recreatetexture) gl.deleteTexture(t); throw love::Exception("Could not create font texture!"); } textureWidth = size.width; textureHeight = size.height; rowHeight = textureX = textureY = TEXTURE_PADDING; prevmemsize = textureMemorySize; textureMemorySize += emptydata.size(); gl.updateTextureMemorySize(prevmemsize, textureMemorySize); // Re-add the old glyphs if we re-created the existing texture object. if (recreatetexture) { textureCacheID++; std::vector glyphstoadd; for (const auto &glyphpair : glyphs) glyphstoadd.push_back(glyphpair.first); glyphs.clear(); for (uint32 g : glyphstoadd) addGlyph(g); } else textures.push_back(t); } love::font::GlyphData *Font::getRasterizerGlyphData(uint32 glyph) { // Use spaces for the tab 'glyph'. if (glyph == 9 && useSpacesAsTab) { love::font::GlyphData *spacegd = rasterizers[0]->getGlyphData(32); love::font::GlyphData::Format fmt = spacegd->getFormat(); love::font::GlyphMetrics gm = {}; gm.advance = spacegd->getAdvance() * SPACES_PER_TAB; gm.bearingX = spacegd->getBearingX(); gm.bearingY = spacegd->getBearingY(); spacegd->release(); return new love::font::GlyphData(glyph, gm, fmt); } for (const StrongRef &r : rasterizers) { if (r->hasGlyph(glyph)) return r->getGlyphData(glyph); } return rasterizers[0]->getGlyphData(glyph); } const Font::Glyph &Font::addGlyph(uint32 glyph) { love::font::GlyphData *gd = getRasterizerGlyphData(glyph); int w = gd->getWidth(); int h = gd->getHeight(); if (textureX + w + TEXTURE_PADDING > textureWidth) { // out of space - new row! textureX = TEXTURE_PADDING; textureY += rowHeight; rowHeight = TEXTURE_PADDING; } if (textureY + h + TEXTURE_PADDING > textureHeight) { // totally out of space - new texture! try { createTexture(); } catch (love::Exception &) { gd->release(); throw; } } Glyph g; g.texture = 0; g.spacing = gd->getAdvance(); memset(g.vertices, 0, sizeof(GlyphVertex) * 4); // don't waste space for empty glyphs. also fixes a divide by zero bug with ATI drivers if (w > 0 && h > 0) { const GLuint t = textures.back(); gl.bindTexture(t); glTexSubImage2D(GL_TEXTURE_2D, 0, textureX, textureY, w, h, (type == FONT_TRUETYPE ? GL_LUMINANCE_ALPHA : GL_RGBA), GL_UNSIGNED_BYTE, gd->getData()); g.texture = t; float tX = (float) textureX, tY = (float) textureY; float tWidth = (float) textureWidth, tHeight = (float) textureHeight; // 0----2 // | / | // | / | // 1----3 const GlyphVertex verts[4] = { { 0.0f, 0.0f, tX/tWidth, tY/tHeight}, { 0.0f, float(h), tX/tWidth, (tY+h)/tHeight}, {float(w), 0.0f, (tX+w)/tWidth, tY/tHeight}, {float(w), float(h), (tX+w)/tWidth, (tY+h)/tHeight} }; // Copy vertex data to the glyph and set proper bearing. for (int i = 0; i < 4; i++) { g.vertices[i] = verts[i]; g.vertices[i].x += gd->getBearingX(); g.vertices[i].y -= gd->getBearingY(); } } if (w > 0) textureX += (w + TEXTURE_PADDING); if (h > 0) rowHeight = std::max(rowHeight, h + TEXTURE_PADDING); gd->release(); const auto p = glyphs.insert(std::make_pair(glyph, g)); return p.first->second; } const Font::Glyph &Font::findGlyph(uint32 glyph) { const auto it = glyphs.find(glyph); if (it != glyphs.end()) return it->second; return addGlyph(glyph); } float Font::getKerning(uint32 leftglyph, uint32 rightglyph) { uint64 packedglyphs = ((uint64) leftglyph << 32) | (uint64) rightglyph; const auto it = kerning.find(packedglyphs); if (it != kerning.end()) return it->second; float k = rasterizers[0]->getKerning(leftglyph, rightglyph); for (const auto &r : rasterizers) { if (r->hasGlyph(leftglyph) && r->hasGlyph(rightglyph)) { k = r->getKerning(leftglyph, rightglyph); break; } } kerning[packedglyphs] = k; return k; } float Font::getHeight() const { return (float) height; } std::vector Font::generateVertices(const Codepoints &codepoints, std::vector &vertices, float extra_spacing, Vector offset, TextInfo *info) { // Spacing counter and newline handling. float dx = offset.x; float dy = offset.y; float lineheight = getBaseline(); int maxwidth = 0; // Keeps track of when we need to switch textures in our vertex array. std::vector drawcommands; // Pre-allocate space for the maximum possible number of vertices. size_t vertstartsize = vertices.size(); vertices.reserve(vertstartsize + codepoints.size()); uint32 prevglyph = 0; for (int i = 0; i < (int) codepoints.size(); i++) { uint32 g = codepoints[i]; if (g == '\n') { if (dx > maxwidth) maxwidth = (int) dx; // Wrap newline, but do not print it. dy += floorf(getHeight() * getLineHeight() + 0.5f); dx = offset.x; continue; } uint32 cacheid = textureCacheID; const Glyph &glyph = findGlyph(g); // If findGlyph invalidates the texture cache, re-start the loop. if (cacheid != textureCacheID) { i = 0; maxwidth = 0; dx = offset.x; dy = offset.y; drawcommands.clear(); vertices.resize(vertstartsize); prevglyph = 0; continue; } // Add kerning to the current horizontal offset. dx += getKerning(prevglyph, g); if (glyph.texture != 0) { // Copy the vertices and set their proper relative positions. for (int j = 0; j < 4; j++) { vertices.push_back(glyph.vertices[j]); vertices.back().x += dx; vertices.back().y += dy + lineheight; } // Check if glyph texture has changed since the last iteration. if (drawcommands.empty() || drawcommands.back().texture != glyph.texture) { // Add a new draw command if the texture has changed. DrawCommand cmd; cmd.startvertex = (int) vertices.size() - 4; cmd.vertexcount = 0; cmd.texture = glyph.texture; drawcommands.push_back(cmd); } drawcommands.back().vertexcount += 4; } // Advance the x position for the next glyph. dx += glyph.spacing; // Account for extra spacing given to space characters. if (g == ' ' && extra_spacing != 0.0f) dx = floorf(dx + extra_spacing); prevglyph = g; } // Sort draw commands by texture first, and quad position in memory second // (using the struct's < operator). std::sort(drawcommands.begin(), drawcommands.end()); if (dx > maxwidth) maxwidth = (int) dx; if (info != nullptr) { info->width = maxwidth - offset.x;; info->height = (int) dy + (dx > 0.0f ? floorf(getHeight() * getLineHeight() + 0.5f) : 0) - offset.y; } return drawcommands; } std::vector Font::generateVertices(const std::string &text, std::vector &vertices, float extra_spacing, Vector offset, TextInfo *info) { Codepoints codepoints; codepoints.reserve(text.size()); try { utf8::iterator i(text.begin(), text.begin(), text.end()); utf8::iterator end(text.end(), text.begin(), text.end()); while (i != end) { uint32 g = *i++; codepoints.push_back(g); } } catch (utf8::exception &e) { throw love::Exception("UTF-8 decoding error: %s", e.what()); } return generateVertices(codepoints, vertices, extra_spacing, offset, info); } std::vector Font::generateVerticesFormatted(const std::string &text, float wrap, AlignMode align, std::vector &vertices, TextInfo *info) { wrap = std::max(wrap, 0.0f); uint32 cacheid = textureCacheID; std::vector drawcommands; vertices.reserve(text.length() * 4); std::vector widths; std::vector lines; getWrap(text, wrap, lines, &widths); float y = 0.0f; float maxwidth = 0.0f; for (int i = 0; i < (int) lines.size(); i++) { const auto &line = lines[i]; float width = (float) widths[i]; love::Vector offset(0.0f, floorf(y)); float extraspacing = 0.0f; maxwidth = std::max(width, maxwidth); switch (align) { case ALIGN_RIGHT: offset.x = floorf(wrap - width); break; case ALIGN_CENTER: offset.x = floorf((wrap - width) / 2.0f); break; case ALIGN_JUSTIFY: { float numspaces = (float) std::count(line.begin(), line.end(), ' '); if (width < wrap && numspaces >= 1) extraspacing = (wrap - width) / numspaces; else extraspacing = 0.0f; break; } case ALIGN_LEFT: default: break; } std::vector commands = generateVertices(line, vertices, extraspacing, offset); if (!commands.empty()) { auto firstcmd = commands.begin(); // If the first draw command in the new list has the same texture // as the last one in the existing list we're building and its // vertices are in-order, we can combine them (saving a draw call.) if (!drawcommands.empty()) { auto prevcmd = drawcommands.back(); if (prevcmd.texture == firstcmd->texture && (prevcmd.startvertex + prevcmd.vertexcount) == firstcmd->startvertex) { drawcommands.back().vertexcount += firstcmd->vertexcount; ++firstcmd; } } // Append the new draw commands to the list we're building. drawcommands.insert(drawcommands.end(), firstcmd, commands.end()); } y += getHeight() * getLineHeight(); } if (info != nullptr) { info->width = (int) maxwidth; info->height = (int) y; } if (cacheid != textureCacheID) { vertices.clear(); drawcommands = generateVerticesFormatted(text, wrap, align, vertices); } return drawcommands; } void Font::drawVertices(const std::vector &drawcommands) { // Vertex attribute pointers need to be set before calling this function. // This assumes that the attribute pointers are constant for all vertices. int totalverts = 0; for (const DrawCommand &cmd : drawcommands) totalverts = std::max(cmd.startvertex + cmd.vertexcount, totalverts); if ((size_t) totalverts / 4 > quadIndices.getSize()) quadIndices = QuadIndices((size_t) totalverts / 4); gl.prepareDraw(); const GLenum gltype = quadIndices.getType(); const size_t elemsize = quadIndices.getElementSize(); GLBuffer::Bind bind(*quadIndices.getBuffer()); // We need a separate draw call for every section of the text which uses a // different texture than the previous section. for (const DrawCommand &cmd : drawcommands) { GLsizei count = (cmd.vertexcount / 4) * 6; size_t offset = (cmd.startvertex / 4) * 6 * elemsize; // TODO: Use glDrawElementsBaseVertex when supported? gl.bindTexture(cmd.texture); gl.drawElements(GL_TRIANGLES, count, gltype, quadIndices.getPointer(offset)); } } void Font::printv(const Matrix4 &t, const std::vector &drawcommands, const std::vector &vertices) { if (vertices.empty() || drawcommands.empty()) return; OpenGL::TempDebugGroup debuggroup("Font print"); OpenGL::TempTransform transform(gl); transform.get() *= t; gl.useVertexAttribArrays(ATTRIBFLAG_POS | ATTRIBFLAG_TEXCOORD); glVertexAttribPointer(ATTRIB_POS, 2, GL_FLOAT, GL_FALSE, sizeof(GlyphVertex), &vertices[0].x); glVertexAttribPointer(ATTRIB_TEXCOORD, 2, GL_FLOAT, GL_FALSE, sizeof(GlyphVertex), &vertices[0].s); drawVertices(drawcommands); } void Font::print(const std::string &text, float x, float y, float angle, float sx, float sy, float ox, float oy, float kx, float ky) { std::vector vertices; std::vector drawcommands = generateVertices(text, vertices); Matrix4 t(ceilf(x), ceilf(y), angle, sx, sy, ox, oy, kx, ky); printv(t, drawcommands, vertices); } void Font::printf(const std::string &text, float x, float y, float wrap, AlignMode align, float angle, float sx, float sy, float ox, float oy, float kx, float ky) { std::vector vertices; std::vector drawcommands = generateVerticesFormatted(text, wrap, align, vertices); Matrix4 t(ceilf(x), ceilf(y), angle, sx, sy, ox, oy, kx, ky); printv(t, drawcommands, vertices); } int Font::getWidth(const std::string &str) { if (str.size() == 0) return 0; std::istringstream iss(str); std::string line; int max_width = 0; while (getline(iss, line, '\n')) { int width = 0; uint32 prevglyph = 0; try { utf8::iterator i(line.begin(), line.begin(), line.end()); utf8::iterator end(line.end(), line.begin(), line.end()); while (i != end) { uint32 c = *i++; const Glyph &g = findGlyph(c); width += g.spacing + getKerning(prevglyph, c); prevglyph = c; } } catch (utf8::exception &e) { throw love::Exception("UTF-8 decoding error: %s", e.what()); } max_width = std::max(max_width, width); } return max_width; } int Font::getWidth(char character) { const Glyph &g = findGlyph(character); return g.spacing; } void Font::getWrap(const std::string &text, float wraplimit, std::vector &lines, std::vector *linewidths) { // Split text at newlines. std::string line; std::istringstream iss(text); // A wrapped line of text. Codepoints wline; while (std::getline(iss, line, '\n')) { float width = 0.0f; uint32 prevglyph = 0; wline.clear(); wline.reserve(line.length()); try { utf8::iterator i(line.begin(), line.begin(), line.end()); utf8::iterator end(line.end(), line.begin(), line.end()); utf8::iterator lastspaceit = end; while (i != end) { uint32 c = *i; const Glyph &g = findGlyph(c); float newwidth = width + g.spacing + getKerning(prevglyph, c); // Wrap the line if it exceeds the wrap limit. Don't wrap yet if // we're processing a newline character, though. if (c != ' ' && newwidth > wraplimit) { // If this is the first character in the line and it exceeds // the limit, skip it completely. if (wline.empty()) ++i; else if (lastspaceit != end) { // 'Rewind' to the last seen space, if the line has one. // FIXME: This should preferably use vector::erase. while (!wline.empty() && wline.back() != ' ') wline.pop_back(); i = lastspaceit; ++i; // Start the next line after the space. } lines.push_back(wline); if (linewidths) linewidths->push_back(width); prevglyph = 0; // Reset kerning information. width = 0.0f; wline.clear(); lastspaceit = end; continue; } width = newwidth; prevglyph = c; wline.push_back(c); // Keep track of the last seen space, so we can "rewind" to it // when wrapping. if (c == ' ') lastspaceit = i; ++i; } } catch (utf8::exception &e) { throw love::Exception("UTF-8 decoding error: %s", e.what()); } // Remove trailing newlines. if (!wline.empty() && wline.back() == '\n') wline.pop_back(); lines.push_back(wline); if (linewidths) linewidths->push_back(width); } } void Font::getWrap(const std::string &text, float wraplimit, std::vector &lines, std::vector *linewidths) { std::vector codepointlines; getWrap(text, wraplimit, codepointlines, linewidths); std::string line; for (const Codepoints &codepoints : codepointlines) { line.clear(); line.reserve(codepoints.size()); for (uint32 codepoint : codepoints) { char character[5] = {'\0'}; char *end = utf8::unchecked::append(codepoint, character); line.append(character, end - character); } lines.push_back(line); } } void Font::setLineHeight(float height) { lineHeight = height; } float Font::getLineHeight() const { return lineHeight; } void Font::setFilter(const Texture::Filter &f) { if (!Texture::validateFilter(f, false)) throw love::Exception("Invalid texture filter."); filter = f; for (GLuint texture : textures) { gl.bindTexture(texture); gl.setTextureFilter(filter); } } const Texture::Filter &Font::getFilter() { return filter; } bool Font::loadVolatile() { createTexture(); textureCacheID++; return true; } void Font::unloadVolatile() { // nuke everything from orbit glyphs.clear(); for (GLuint texture : textures) gl.deleteTexture(texture); textures.clear(); gl.updateTextureMemorySize(textureMemorySize, 0); textureMemorySize = 0; } int Font::getAscent() const { return rasterizers[0]->getAscent(); } int Font::getDescent() const { return rasterizers[0]->getDescent(); } float Font::getBaseline() const { // 1.25 is magic line height for true type fonts return (type == FONT_TRUETYPE) ? floorf(getHeight() / 1.25f + 0.5f) : 0.0f; } bool Font::hasGlyph(uint32 glyph) const { for (const StrongRef &r : rasterizers) { if (r->hasGlyph(glyph)) return true; } return false; } bool Font::hasGlyphs(const std::string &text) const { if (text.size() == 0) return false; try { utf8::iterator i(text.begin(), text.begin(), text.end()); utf8::iterator end(text.end(), text.begin(), text.end()); while (i != end) { uint32 codepoint = *i++; if (!hasGlyph(codepoint)) return false; } } catch (utf8::exception &e) { throw love::Exception("UTF-8 decoding error: %s", e.what()); } return true; } void Font::setFallbacks(const std::vector &fallbacks) { for (const Font *f : fallbacks) { if (f->type != this->type) throw love::Exception("Font fallbacks must be of the same font type."); } rasterizers.resize(1); // NOTE: this won't invalidate already-rasterized glyphs. for (const Font *f : fallbacks) rasterizers.push_back(f->rasterizers[0]); } uint32 Font::getTextureCacheID() const { return textureCacheID; } bool Font::getConstant(const char *in, AlignMode &out) { return alignModes.find(in, out); } bool Font::getConstant(AlignMode in, const char *&out) { return alignModes.find(in, out); } StringMap::Entry Font::alignModeEntries[] = { { "left", Font::ALIGN_LEFT }, { "right", Font::ALIGN_RIGHT }, { "center", Font::ALIGN_CENTER }, { "justify", Font::ALIGN_JUSTIFY }, }; StringMap Font::alignModes(Font::alignModeEntries, sizeof(Font::alignModeEntries)); } // opengl } // graphics } // love