Commits

Alex Szpakowski committed 2902c71

Text drawing performance improvements (issue #532)

Comments (0)

Files changed (3)

src/modules/graphics/opengl/Font.cpp

 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)
 	: rasterizer(r)
 	, height(r->getHeight())
 	, filter(filter)
 {
 	r->retain();
+	
 	love::font::GlyphData *gd = r->getGlyphData(32);
 	type = (gd->getFormat() == love::font::GlyphData::FORMAT_LUMINANCE_ALPHA ? FONT_TRUETYPE : FONT_IMAGE);
 	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()
 	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()
 {
 	texture_x = texture_y = rowHeight = TEXTURE_PADDING;
+	
 	GLuint t;
 	glGenTextures(1, &t);
 	textures.push_back(t);
 	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, 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);
-	// Initialize the texture
-	glTexImage2D(GL_TEXTURE_2D,
-				 0,
-				 GL_RGBA,
-				 (GLsizei)TEXTURE_WIDTH,
-				 (GLsizei)TEXTURE_HEIGHT,
-				 0,
-				 format,
-				 GL_UNSIGNED_BYTE,
-				 NULL);
+	
+	
+	// try to initialize the texture, attempting smaller sizes if initialization fails
+	bool initialized = false;
+	while (texture_size_index >= 0)
+	{
+		texture_width = TEXTURE_WIDTHS[texture_size_index];
+		texture_height = TEXTURE_HEIGHTS[texture_size_index];
+		
+		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
-	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,
 					0,
-					0,
-					0,
-					(GLsizei)TEXTURE_WIDTH,
-					(GLsizei)TEXTURE_HEIGHT,
+					0, 0,
+					(GLsizei)texture_width,
+					(GLsizei)texture_height,
 					format,
 					GL_UNSIGNED_BYTE,
 					&emptyData[0]);
 }
 
-Font::Glyph *Font::addGlyph(int glyph)
+Font::Glyph *Font::addGlyph(const int glyph)
 {
 	love::font::GlyphData *gd = rasterizer->getGlyphData(glyph);
 	int w = gd->getWidth();
 	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!
 		texture_x = TEXTURE_PADDING;
 		texture_y += rowHeight;
 		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!
 		createTexture();
 	}
 
 	Glyph *g = new Glyph;
-	g->list = g->texture = 0;
+	
+	g->texture = 0;
 	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
 	if (w > 0 && h > 0)
 	{
-		g->list = glGenLists(1);
-		if (0 == g->list)
-		{
-			delete g;
-			return NULL;
-		}
-
-		GLuint t = textures.back();
+		const GLuint t = textures.back();
+		
 		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;
 
 		v.y = (float) texture_y;
 		v.w = (float) w;
 		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);
-		glEnableClientState(GL_TEXTURE_COORD_ARRAY);
-		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);
-		glPushMatrix();
-		glTranslatef(static_cast<float>(gd->getBearingX()), static_cast<float>(-gd->getBearingY()), 0.0f);
-		glDrawArrays(GL_QUADS, 0, 4);
-		glPopMatrix();
-		glEndList();
-
-		glDisableClientState(GL_TEXTURE_COORD_ARRAY);
-		glDisableClientState(GL_VERTEX_ARRAY);
-
-		delete q;
+		
+		Quad q = Quad(v, (const float) texture_width, (const float) texture_height);
+		const vertex *verts = q.getVertices();
+		
+		// copy vertex data to the glyph and set proper bearing
+		for (int i = 0; i < 4; i++)
+		{
+			g->quad.vertices[i] = verts[i];
+			g->quad.vertices[i].x += gd->getBearingX();
+			g->quad.vertices[i].y -= gd->getBearingY();
+		}
 	}
 
 	if (w > 0)
 		rowHeight = std::max(rowHeight, h + TEXTURE_PADDING);
 
 	delete gd;
+	
 	glyphs[glyph] = g;
+	
+	return g;
+}
+
+Font::Glyph *Font::findGlyph(const int glyph)
+{
+	Glyph *g = glyphs[glyph];
+	if (!g)
+		g = addGlyph(glyph);
+	
 	return g;
 }
 
 	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 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();
 
 	Matrix t;
 	t.setTransformation(ceil(x), ceil(y), angle, sx, sy, ox, oy, kx, ky);
 	glMultMatrixf((const GLfloat *)t.getElements());
+
 	try
 	{
-		utf8::iterator<std::string::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> i(text.begin(), text.begin(), text.end());
+		utf8::iterator<std::string::const_iterator> end(text.end(), text.begin(), text.end());
+		
 		while (i != end)
 		{
 			int g = *i++;
+			
 			if (g == '\n')
 			{
 				// wrap newline, but do not print it
-				glTranslatef(-dx, floor(getHeight() * getLineHeight() + 0.5f), 0);
+				dy += floor(getHeight() * getLineHeight() + 0.5f);
 				dx = 0.0f;
 				continue;
 			}
-			Glyph *glyph = glyphs[g];
-			if (!glyph) glyph = addGlyph(g);
-			glPushMatrix();
-			// 1.25 is magic line height for true type fonts
-			if (type == FONT_TRUETYPE) glTranslatef(0, floor(getHeight() / 1.25f + 0.5f), 0);
-			bindTexture(glyph->texture);
-			glCallList(glyph->list);
-			glPopMatrix();
-			glTranslatef(static_cast<GLfloat>(glyph->spacing + letter_spacing), 0, 0);
+			
+			Glyph *glyph = findGlyph(g);
+			
+			// we only care about the vertices of glyphs which have a texture
+			if (glyph->texture != 0)
+			{
+				// copy glyphquad (4 vertices) from original glyph to our current quad list
+				glyphquads.push_back(glyph->quad);
+				
+				// 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;
 		}
 	}
-	catch(utf8::exception &e)
+	catch (love::Exception &e)
+	{
+		glPopMatrix();
+		throw;
+	}
+	catch (utf8::exception &e)
 	{
 		glPopMatrix();
 		throw love::Exception("%s", e.what());
 	}
+	
+	if (quadindex > 0 && glyphinfolist.size() > 0)
+	{		
+		// sort glyph draw info list by texture first, and quad position in memory second (using the struct's < operator)
+		std::sort(glyphinfolist.begin(), glyphinfolist.end());
+		
+		glEnableClientState(GL_VERTEX_ARRAY);
+		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();
 }
 
-void Font::print(char character, float x, float y)
-{
-	Glyph *glyph = glyphs[character];
-	if (!glyph) glyph = addGlyph(character);
-
-	if (0 != glyph->texture)
-	{
-		glPushMatrix();
-		glTranslatef(x, floor(y+getHeight() + 0.5f), 0.0f);
-		bindTexture(glyph->texture);
-		glCallList(glyph->list);
-		glPopMatrix();
-	}
-}
-
 int Font::getWidth(const std::string &str)
 {
 	if (str.size() == 0) return 0;
 			while (i != end)
 			{
 				int c = *i++;
-				g = glyphs[c];
-				if (!g) g = addGlyph(c);
+				g = findGlyph(c);
 				width += static_cast<int>(g->spacing * mSpacing);
 			}
 		}
 
 int Font::getWidth(const char character)
 {
-	Glyph *g = glyphs[character];
-	if (!g) g = addGlyph(character);
+	Glyph *g = findGlyph(character);
 	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;
 	const float width_space = static_cast<float>(getWidth(' '));
 	while (it != glyphs.end())
 	{
 		g = it->second;
-		glDeleteLists(g->list, 1);
 		delete g;
 		glyphs.erase(it++);
 	}

src/modules/graphics/opengl/Font.h

 #include "graphics/Image.h"
 
 #include "OpenGL.h"
-#include "GLee.h"
 
 namespace love
 {
 	 * @param kx Shear along the x 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);
-
-	/**
-	 * 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);
+	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);
 
 	/**
 	 * Returns the height of the font.
 	 * @param max_width Optional output of the maximum width
 	 * 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,
 	// Implements Volatile.
 	bool loadVolatile();
 	void unloadVolatile();
+	
 private:
 
 	enum FontType
 		FONT_IMAGE,
 		FONT_UNKNOWN
 	};
+	
+	// thin wrapper for an array of 4 vertices
+	struct GlyphQuad
+	{
+		vertex vertices[4];
+	};
 
 	struct Glyph
 	{
-		GLuint list;
 		GLuint texture;
 		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;
 	int height;
 	float lineHeight;
 	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::map<int, Glyph *> glyphs; // maps glyphs to display lists
+	std::map<int, Glyph *> glyphs; // maps glyphs to quad information
 	FontType type;
 	Image::Filter filter;
+	
+	static const int NUM_TEXTURE_SIZES = 7;
+	static const int TEXTURE_WIDTHS[NUM_TEXTURE_SIZES];
+	static const int TEXTURE_HEIGHTS[NUM_TEXTURE_SIZES];
 
-	static const int TEXTURE_WIDTH = 512;
-	static const int TEXTURE_HEIGHT = 512;
 	static const int TEXTURE_PADDING = 1;
 
 	int texture_x, texture_y;
 	int rowHeight;
 
+	bool initializeTexture(GLint format);
 	void createTexture();
-	Glyph *addGlyph(int glyph);
+	Glyph *addGlyph(const int glyph);
+	Glyph *findGlyph (const int glyph);
 }; // Font
 
 } // opengl

src/modules/graphics/opengl/wrap_Graphics.cpp

 
 	love::font::Rasterizer *rasterizer = luax_checktype<love::font::Rasterizer>(L, 1, "Rasterizer", FONT_RASTERIZER_T);
 
-	// Create the font.
-	Font *font = instance->newFont(rasterizer, instance->getDefaultImageFilter());
+	Font *font = NULL;
+	try
+	{
+		// Create the font.
+		font = instance->newFont(rasterizer, instance->getDefaultImageFilter());
+	}
+	catch (love::Exception &e)
+	{
+		return luaL_error(L, e.what());
+	}
 
 	if (font == 0)
 		return luaL_error(L, "Could not load font.");