Text drawing performance improvements (issue #532)

This commit is contained in:
Alexander Szpakowski
2012-12-15 22:18:40 +01:00
parent 68a895e7c6
commit a8dc813258
3 changed files with 263 additions and 102 deletions
+208 -78
View File
@@ -39,6 +39,9 @@ namespace graphics
namespace opengl namespace opengl
{ {
const int Font::TEXTURE_WIDTHS[] = {128, 256, 256, 512, 512, 1024, 1024};
const int Font::TEXTURE_HEIGHTS[] = {128, 128, 256, 256, 512, 512, 1024};
Font::Font(love::font::Rasterizer *r, const Image::Filter &filter) Font::Font(love::font::Rasterizer *r, const Image::Filter &filter)
: rasterizer(r) : rasterizer(r)
, height(r->getHeight()) , height(r->getHeight())
@@ -47,10 +50,37 @@ Font::Font(love::font::Rasterizer *r, const Image::Filter &filter)
, filter(filter) , filter(filter)
{ {
r->retain(); r->retain();
love::font::GlyphData *gd = r->getGlyphData(32); love::font::GlyphData *gd = r->getGlyphData(32);
type = (gd->getFormat() == love::font::GlyphData::FORMAT_LUMINANCE_ALPHA ? FONT_TRUETYPE : FONT_IMAGE); type = (gd->getFormat() == love::font::GlyphData::FORMAT_LUMINANCE_ALPHA ? FONT_TRUETYPE : FONT_IMAGE);
delete gd; delete gd;
createTexture();
// try to find the best texture size match for the font size
// default to the largest texture size if no rough match is found
texture_size_index = NUM_TEXTURE_SIZES - 1;
for (int i = 0; i < NUM_TEXTURE_SIZES; i++)
{
// base our chosen texture width/height on a very rough guess of the total size taken up by the font's used glyphs
// the estimate is likely larger than the actual total size taken up, which is good since texture changes are expensive
if ((height * 0.8) * height * 95 <= TEXTURE_WIDTHS[i] * TEXTURE_HEIGHTS[i])
{
texture_size_index = i;
break;
}
}
texture_width = TEXTURE_WIDTHS[texture_size_index];
texture_height = TEXTURE_HEIGHTS[texture_size_index];
try
{
createTexture();
}
catch (love::Exception &e)
{
r->release();
throw;
}
} }
Font::~Font() Font::~Font()
@@ -59,9 +89,30 @@ Font::~Font()
unloadVolatile(); unloadVolatile();
} }
bool Font::initializeTexture(GLint format)
{
GLint internalformat = (format == GL_LUMINANCE_ALPHA) ? GL_LUMINANCE8_ALPHA8 : GL_RGBA8;
// clear errors before initializing
while (glGetError() != GL_NO_ERROR);
glTexImage2D(GL_TEXTURE_2D,
0,
internalformat,
(GLsizei)texture_width,
(GLsizei)texture_height,
0,
format,
GL_UNSIGNED_BYTE,
NULL);
return glGetError() == GL_NO_ERROR;
}
void Font::createTexture() void Font::createTexture()
{ {
texture_x = texture_y = rowHeight = TEXTURE_PADDING; texture_x = texture_y = rowHeight = TEXTURE_PADDING;
GLuint t; GLuint t;
glGenTextures(1, &t); glGenTextures(1, &t);
textures.push_back(t); textures.push_back(t);
@@ -74,65 +125,85 @@ void Font::createTexture()
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
GLint format = (type == FONT_TRUETYPE ? GL_LUMINANCE_ALPHA : GL_RGBA); GLint format = (type == FONT_TRUETYPE ? GL_LUMINANCE_ALPHA : GL_RGBA);
// Initialize the texture
glTexImage2D(GL_TEXTURE_2D,
0, // try to initialize the texture, attempting smaller sizes if initialization fails
GL_RGBA, bool initialized = false;
(GLsizei)TEXTURE_WIDTH, while (texture_size_index >= 0)
(GLsizei)TEXTURE_HEIGHT, {
0, texture_width = TEXTURE_WIDTHS[texture_size_index];
format, texture_height = TEXTURE_HEIGHTS[texture_size_index];
GL_UNSIGNED_BYTE,
NULL); initialized = initializeTexture(format);
if (initialized || texture_size_index <= 0)
break;
--texture_size_index;
}
if (!initialized)
{
// cleanup before throwing
deleteTexture(t);
bindTexture(0);
textures.pop_back();
throw love::Exception("Could not create font texture!");
}
// Fill the texture with transparent black // Fill the texture with transparent black
std::vector<GLubyte> emptyData(TEXTURE_WIDTH * TEXTURE_HEIGHT * (type == FONT_TRUETYPE ? 2 : 4), 0); std::vector<GLubyte> emptyData(texture_width * texture_height * (type == FONT_TRUETYPE ? 2 : 4), 0);
glTexSubImage2D(GL_TEXTURE_2D, glTexSubImage2D(GL_TEXTURE_2D,
0, 0,
0, 0, 0,
0, (GLsizei)texture_width,
(GLsizei)TEXTURE_WIDTH, (GLsizei)texture_height,
(GLsizei)TEXTURE_HEIGHT,
format, format,
GL_UNSIGNED_BYTE, GL_UNSIGNED_BYTE,
&emptyData[0]); &emptyData[0]);
} }
Font::Glyph *Font::addGlyph(int glyph) Font::Glyph *Font::addGlyph(const int glyph)
{ {
love::font::GlyphData *gd = rasterizer->getGlyphData(glyph); love::font::GlyphData *gd = rasterizer->getGlyphData(glyph);
int w = gd->getWidth(); int w = gd->getWidth();
int h = gd->getHeight(); int h = gd->getHeight();
if (texture_x + w + TEXTURE_PADDING > TEXTURE_WIDTH) if (texture_x + w + TEXTURE_PADDING > texture_width)
{ {
// out of space - new row! // out of space - new row!
texture_x = TEXTURE_PADDING; texture_x = TEXTURE_PADDING;
texture_y += rowHeight; texture_y += rowHeight;
rowHeight = TEXTURE_PADDING; rowHeight = TEXTURE_PADDING;
} }
if (texture_y + h + TEXTURE_PADDING > TEXTURE_HEIGHT) if (texture_y + h + TEXTURE_PADDING > texture_height)
{ {
// totally out of space - new texture! // totally out of space - new texture!
createTexture(); createTexture();
} }
Glyph *g = new Glyph; Glyph *g = new Glyph;
g->list = g->texture = 0;
g->texture = 0;
g->spacing = gd->getAdvance(); g->spacing = gd->getAdvance();
memset(&g->quad, 0, sizeof(GlyphQuad));
// don't waste space for empty glyphs. also fixes a division by zero bug with ati drivers // don't waste space for empty glyphs. also fixes a division by zero bug with ati drivers
if (w > 0 && h > 0) if (w > 0 && h > 0)
{ {
g->list = glGenLists(1); const GLuint t = textures.back();
if (0 == g->list)
{
delete g;
return NULL;
}
GLuint t = textures.back();
bindTexture(t); bindTexture(t);
glTexSubImage2D(GL_TEXTURE_2D, 0, texture_x, texture_y, w, h, (type == FONT_TRUETYPE ? GL_LUMINANCE_ALPHA : GL_RGBA), GL_UNSIGNED_BYTE, gd->getData()); glTexSubImage2D(GL_TEXTURE_2D,
0,
texture_x,
texture_y,
w, h,
(type == FONT_TRUETYPE ? GL_LUMINANCE_ALPHA : GL_RGBA),
GL_UNSIGNED_BYTE,
gd->getData());
g->texture = t; g->texture = t;
@@ -141,25 +212,17 @@ Font::Glyph *Font::addGlyph(int glyph)
v.y = (float) texture_y; v.y = (float) texture_y;
v.w = (float) w; v.w = (float) w;
v.h = (float) h; v.h = (float) h;
Quad *q = new Quad(v, (const float) TEXTURE_WIDTH, (const float) TEXTURE_HEIGHT);
const vertex *verts = q->getVertices();
glEnableClientState(GL_VERTEX_ARRAY); Quad q = Quad(v, (const float) texture_width, (const float) texture_height);
glEnableClientState(GL_TEXTURE_COORD_ARRAY); const vertex *verts = q.getVertices();
glVertexPointer(2, GL_FLOAT, sizeof(vertex), (GLvoid *)&verts[0].x);
glTexCoordPointer(2, GL_FLOAT, sizeof(vertex), (GLvoid *)&verts[0].s);
glNewList(g->list, GL_COMPILE); // copy vertex data to the glyph and set proper bearing
glPushMatrix(); for (int i = 0; i < 4; i++)
glTranslatef(static_cast<float>(gd->getBearingX()), static_cast<float>(-gd->getBearingY()), 0.0f); {
glDrawArrays(GL_QUADS, 0, 4); g->quad.vertices[i] = verts[i];
glPopMatrix(); g->quad.vertices[i].x += gd->getBearingX();
glEndList(); g->quad.vertices[i].y -= gd->getBearingY();
}
glDisableClientState(GL_TEXTURE_COORD_ARRAY);
glDisableClientState(GL_VERTEX_ARRAY);
delete q;
} }
if (w > 0) if (w > 0)
@@ -168,7 +231,18 @@ Font::Glyph *Font::addGlyph(int glyph)
rowHeight = std::max(rowHeight, h + TEXTURE_PADDING); rowHeight = std::max(rowHeight, h + TEXTURE_PADDING);
delete gd; delete gd;
glyphs[glyph] = g; glyphs[glyph] = g;
return g;
}
Font::Glyph *Font::findGlyph(const int glyph)
{
Glyph *g = glyphs[glyph];
if (!g)
g = addGlyph(glyph);
return g; return g;
} }
@@ -177,61 +251,120 @@ float Font::getHeight() const
return static_cast<float>(height); return static_cast<float>(height);
} }
void Font::print(std::string text, float x, float y, float letter_spacing, float angle, float sx, float sy, float ox, float oy, float kx, float ky) void Font::print(const std::string &text, float x, float y, float letter_spacing, float angle, float sx, float sy, float ox, float oy, float kx, float ky)
{ {
float dx = 0.0f; // spacing counter for newline handling float dx = 0.0f; // spacing counter for newline handling
float dy = 0.0f;
// keeps track of when we need to switch textures in our vertex array
std::vector<GlyphArrayDrawInfo> glyphinfolist;
std::vector<GlyphQuad> glyphquads;
glyphquads.reserve(text.size()); // pre-allocate space for the maximum possible number of quads
int quadindex = 0;
glPushMatrix(); glPushMatrix();
Matrix t; Matrix t;
t.setTransformation(ceil(x), ceil(y), angle, sx, sy, ox, oy, kx, ky); t.setTransformation(ceil(x), ceil(y), angle, sx, sy, ox, oy, kx, ky);
glMultMatrixf((const GLfloat *)t.getElements()); glMultMatrixf((const GLfloat *)t.getElements());
try try
{ {
utf8::iterator<std::string::iterator> i(text.begin(), text.begin(), text.end()); utf8::iterator<std::string::const_iterator> i(text.begin(), text.begin(), text.end());
utf8::iterator<std::string::iterator> end(text.end(), text.begin(), text.end()); utf8::iterator<std::string::const_iterator> end(text.end(), text.begin(), text.end());
while (i != end) while (i != end)
{ {
int g = *i++; int g = *i++;
if (g == '\n') if (g == '\n')
{ {
// wrap newline, but do not print it // wrap newline, but do not print it
glTranslatef(-dx, floor(getHeight() * getLineHeight() + 0.5f), 0); dy += floor(getHeight() * getLineHeight() + 0.5f);
dx = 0.0f; dx = 0.0f;
continue; continue;
} }
Glyph *glyph = glyphs[g];
if (!glyph) glyph = addGlyph(g); Glyph *glyph = findGlyph(g);
glPushMatrix();
// 1.25 is magic line height for true type fonts // we only care about the vertices of glyphs which have a texture
if (type == FONT_TRUETYPE) glTranslatef(0, floor(getHeight() / 1.25f + 0.5f), 0); if (glyph->texture != 0)
bindTexture(glyph->texture); {
glCallList(glyph->list); // copy glyphquad (4 vertices) from original glyph to our current quad list
glPopMatrix(); glyphquads.push_back(glyph->quad);
glTranslatef(static_cast<GLfloat>(glyph->spacing + letter_spacing), 0, 0);
// 1.25 is magic line height for true type fonts
float lineheight = (type == FONT_TRUETYPE) ? floor(getHeight() / 1.25f + 0.5f) : 0.0f;
// set proper relative position
for (int i = 0; i < 4; i++)
{
glyphquads[quadindex].vertices[i].x += dx;
glyphquads[quadindex].vertices[i].y += dy + lineheight;
}
size_t listsize = glyphinfolist.size();
// check if current glyph texture has changed since the previous iteration
if (listsize == 0 || glyphinfolist[listsize-1].texture != glyph->texture)
{
// keep track of each sub-section of the string whose glyphs use different textures than the previous section
GlyphArrayDrawInfo glyphdrawinfo;
glyphdrawinfo.startquad = quadindex;
glyphdrawinfo.numquads = 0;
glyphdrawinfo.texture = glyph->texture;
glyphinfolist.push_back(glyphdrawinfo);
}
++quadindex;
++glyphinfolist[glyphinfolist.size()-1].numquads;
}
// advance the x position for the next glyph
dx += glyph->spacing + letter_spacing; dx += glyph->spacing + letter_spacing;
} }
} }
catch(utf8::exception &e) catch (love::Exception &e)
{
glPopMatrix();
throw;
}
catch (utf8::exception &e)
{ {
glPopMatrix(); glPopMatrix();
throw love::Exception("%s", e.what()); throw love::Exception("%s", e.what());
} }
glPopMatrix();
}
void Font::print(char character, float x, float y) if (quadindex > 0 && glyphinfolist.size() > 0)
{
Glyph *glyph = glyphs[character];
if (!glyph) glyph = addGlyph(character);
if (0 != glyph->texture)
{ {
glPushMatrix(); // sort glyph draw info list by texture first, and quad position in memory second (using the struct's < operator)
glTranslatef(x, floor(y+getHeight() + 0.5f), 0.0f); std::sort(glyphinfolist.begin(), glyphinfolist.end());
bindTexture(glyph->texture);
glCallList(glyph->list); glEnableClientState(GL_VERTEX_ARRAY);
glPopMatrix(); glEnableClientState(GL_TEXTURE_COORD_ARRAY);
glVertexPointer(2, GL_FLOAT, sizeof(vertex), (GLvoid *)&glyphquads[0].vertices[0].x);
glTexCoordPointer(2, GL_FLOAT, sizeof(vertex), (GLvoid *)&glyphquads[0].vertices[0].s);
// we need to draw a new vertex array for every section of the string that uses a different texture than the previous section
std::vector<GlyphArrayDrawInfo>::const_iterator it;
for (it = glyphinfolist.begin(); it != glyphinfolist.end(); ++it)
{
bindTexture(it->texture);
int startvertex = it->startquad * 4;
int numvertices = it->numquads * 4;
glDrawArrays(GL_QUADS, startvertex, numvertices);
}
glDisableClientState(GL_TEXTURE_COORD_ARRAY);
glDisableClientState(GL_VERTEX_ARRAY);
} }
glPopMatrix();
} }
int Font::getWidth(const std::string &str) int Font::getWidth(const std::string &str)
@@ -253,8 +386,7 @@ int Font::getWidth(const std::string &str)
while (i != end) while (i != end)
{ {
int c = *i++; int c = *i++;
g = glyphs[c]; g = findGlyph(c);
if (!g) g = addGlyph(c);
width += static_cast<int>(g->spacing * mSpacing); width += static_cast<int>(g->spacing * mSpacing);
} }
} }
@@ -277,12 +409,11 @@ int Font::getWidth(const char *str)
int Font::getWidth(const char character) int Font::getWidth(const char character)
{ {
Glyph *g = glyphs[character]; Glyph *g = findGlyph(character);
if (!g) g = addGlyph(character);
return g->spacing; return g->spacing;
} }
std::vector<std::string> Font::getWrap(const std::string text, float wrap, int *max_width) std::vector<std::string> Font::getWrap(const std::string &text, float wrap, int *max_width)
{ {
using namespace std; using namespace std;
const float width_space = static_cast<float>(getWidth(' ')); const float width_space = static_cast<float>(getWidth(' '));
@@ -376,7 +507,6 @@ void Font::unloadVolatile()
while (it != glyphs.end()) while (it != glyphs.end())
{ {
g = it->second; g = it->second;
glDeleteLists(g->list, 1);
delete g; delete g;
glyphs.erase(it++); glyphs.erase(it++);
} }
+40 -17
View File
@@ -32,7 +32,6 @@
#include "graphics/Image.h" #include "graphics/Image.h"
#include "OpenGL.h" #include "OpenGL.h"
#include "GLee.h"
namespace love namespace love
{ {
@@ -69,16 +68,7 @@ public:
* @param kx Shear along the x axis. * @param kx Shear along the x axis.
* @param ky Shear along the y axis. * @param ky Shear along the y axis.
**/ **/
void print(std::string text, float x, float y, float letter_spacing = 0.0f, float angle = 0.0f, float sx = 1.0f, float sy = 1.0f, float ox = 0.0f, float oy = 0.0f, float kx = 0.0f, float ky = 0.0f); void print(const std::string &text, float x, float y, float letter_spacing = 0.0f, float angle = 0.0f, float sx = 1.0f, float sy = 1.0f, float ox = 0.0f, float oy = 0.0f, float kx = 0.0f, float ky = 0.0f);
/**
* Prints the character at the designated position.
*
* @param character A character.
* @param x The x-coordinate.
* @param y The y-coordinate.
**/
void print(char character, float x, float y);
/** /**
* Returns the height of the font. * Returns the height of the font.
@@ -109,7 +99,7 @@ public:
* @param max_width Optional output of the maximum width * @param max_width Optional output of the maximum width
* Returns a vector with the lines. * Returns a vector with the lines.
**/ **/
std::vector<std::string> getWrap(const std::string text, float wrap, int *max_width = 0); std::vector<std::string> getWrap(const std::string &text, float wrap, int *max_width = 0);
/** /**
* Sets the line height (which should be a number to multiply the font size by, * Sets the line height (which should be a number to multiply the font size by,
@@ -139,6 +129,7 @@ public:
// Implements Volatile. // Implements Volatile.
bool loadVolatile(); bool loadVolatile();
void unloadVolatile(); void unloadVolatile();
private: private:
enum FontType enum FontType
@@ -148,11 +139,34 @@ private:
FONT_UNKNOWN FONT_UNKNOWN
}; };
// thin wrapper for an array of 4 vertices
struct GlyphQuad
{
vertex vertices[4];
};
struct Glyph struct Glyph
{ {
GLuint list;
GLuint texture; GLuint texture;
int spacing; int spacing;
GlyphQuad quad;
};
// used to determine when to change textures in the vertex array generated when printing text
struct GlyphArrayDrawInfo
{
GLuint texture;
int startquad, numquads;
// used when sorting with std::sort
// sorts by texture first (binding textures is expensive) and relative position in memory second
bool operator < (const GlyphArrayDrawInfo &other) const
{
if (texture != other.texture)
return texture < other.texture;
else
return startquad < other.startquad;
};
}; };
love::font::Rasterizer *rasterizer; love::font::Rasterizer *rasterizer;
@@ -160,20 +174,29 @@ private:
int height; int height;
float lineHeight; float lineHeight;
float mSpacing; // modifies the spacing by multiplying it with this value float mSpacing; // modifies the spacing by multiplying it with this value
int texture_size_index;
int texture_width;
int texture_height;
std::vector<GLuint> textures; // vector of packed textures std::vector<GLuint> textures; // vector of packed textures
std::map<int, Glyph *> glyphs; // maps glyphs to display lists std::map<int, Glyph *> glyphs; // maps glyphs to quad information
FontType type; FontType type;
Image::Filter filter; Image::Filter filter;
static const int TEXTURE_WIDTH = 512; static const int NUM_TEXTURE_SIZES = 7;
static const int TEXTURE_HEIGHT = 512; static const int TEXTURE_WIDTHS[NUM_TEXTURE_SIZES];
static const int TEXTURE_HEIGHTS[NUM_TEXTURE_SIZES];
static const int TEXTURE_PADDING = 1; static const int TEXTURE_PADDING = 1;
int texture_x, texture_y; int texture_x, texture_y;
int rowHeight; int rowHeight;
bool initializeTexture(GLint format);
void createTexture(); void createTexture();
Glyph *addGlyph(int glyph); Glyph *addGlyph(const int glyph);
Glyph *findGlyph (const int glyph);
}; // Font }; // Font
} // opengl } // opengl
+10 -2
View File
@@ -290,8 +290,16 @@ int w_newFont(lua_State *L)
love::font::Rasterizer *rasterizer = luax_checktype<love::font::Rasterizer>(L, 1, "Rasterizer", FONT_RASTERIZER_T); love::font::Rasterizer *rasterizer = luax_checktype<love::font::Rasterizer>(L, 1, "Rasterizer", FONT_RASTERIZER_T);
// Create the font. Font *font = NULL;
Font *font = instance->newFont(rasterizer, instance->getDefaultImageFilter()); try
{
// Create the font.
font = instance->newFont(rasterizer, instance->getDefaultImageFilter());
}
catch (love::Exception &e)
{
return luaL_error(L, e.what());
}
if (font == 0) if (font == 0)
return luaL_error(L, "Could not load font."); return luaL_error(L, "Could not load font.");