shithub: cstory

Download patch

ref: 096fa3b578a05687814abdc65107bf05fadaeea7
parent: 1dcf3333fc7d0b0077aff23bf5278cbd45b9d74d
author: Clownacy <Clownacy@users.noreply.github.com>
date: Mon Sep 7 19:11:01 EDT 2020

New custom font batcher

Previously, the font batching system was very shoddy: basically,
glyph bitmaps would be cached to system memory, and uploaded
on-demand to the GPU.

The OpenGL3, OpenGLES2, and SDLTexture backends relied on
`cute_spritebatch.h` to handle batching. While nice, this library is
overkill: it's intended for managing a whole game, not just a couple
of glyphs. It also depends on C++11, which is something I'd rather be
without.

Being so complex, it appears that internally it spams the backend
when merging atlases together. According to bug reports, this causes
the game to skip a lot of frames.

Instead, I've ditched the system-memory-side caching, and made the
game maintain its own atlas. Unlike `cute_spritebatch.h`, this one
doesn't purge glyphs after they've gone unused for 30(?) seconds -
instead, glyphs are only purged when room in the atlas runs out, at
which point the oldest glyph is replaced. This method doesn't involve
creating or destroying atlases at runtime, avoiding the overhead of
whatever OpenGL/DirectX do internally when that happens.

Currently only the OpenGL3, OpenGLES2, and Software renderers have
been updated with this - the others will fail to build.

I know immediate-mode renderers won't benefit from this, but it
replaces the leaky old caching system, so it's still an improvement.

--- a/src/Backends/Rendering.h
+++ b/src/Backends/Rendering.h
@@ -1,7 +1,9 @@
 #pragma once
 
+#include <stddef.h>
+
 typedef struct RenderBackend_Surface RenderBackend_Surface;
-typedef struct RenderBackend_Glyph RenderBackend_Glyph;
+typedef struct RenderBackend_GlyphAtlas RenderBackend_GlyphAtlas;
 
 typedef struct RenderBackend_Rect
 {
@@ -22,10 +24,11 @@
 void RenderBackend_UnlockSurface(RenderBackend_Surface *surface, unsigned int width, unsigned int height);
 void RenderBackend_Blit(RenderBackend_Surface *source_surface, const RenderBackend_Rect *rect, RenderBackend_Surface *destination_surface, long x, long y, bool colour_key);
 void RenderBackend_ColourFill(RenderBackend_Surface *surface, const RenderBackend_Rect *rect, unsigned char red, unsigned char green, unsigned char blue);
-RenderBackend_Glyph* RenderBackend_LoadGlyph(const unsigned char *pixels, unsigned int width, unsigned int height, int pitch);
-void RenderBackend_UnloadGlyph(RenderBackend_Glyph *glyph);
+RenderBackend_GlyphAtlas* RenderBackend_CreateGlyphAtlas(size_t size);
+void RenderBackend_DestroyGlyphAtlas(RenderBackend_GlyphAtlas *atlas);
+void RenderBackend_UploadGlyph(RenderBackend_GlyphAtlas *atlas, size_t x, size_t y, const unsigned char *pixels, size_t width, size_t height);
 void RenderBackend_PrepareToDrawGlyphs(RenderBackend_Surface *destination_surface, const unsigned char *colour_channels);
-void RenderBackend_DrawGlyph(RenderBackend_Glyph *glyph, long x, long y);
+void RenderBackend_DrawGlyph(RenderBackend_GlyphAtlas *atlas, long x, long y, size_t glyph_x, size_t glyph_y, size_t glyph_width, size_t glyph_height);
 void RenderBackend_FlushGlyphs(void);
 void RenderBackend_HandleRenderTargetLoss(void);
 void RenderBackend_HandleWindowResize(unsigned int width, unsigned int height);
--- a/src/Backends/Rendering/OpenGL3.cpp
+++ b/src/Backends/Rendering/OpenGL3.cpp
@@ -12,9 +12,6 @@
 #include <glad/glad.h>
 #endif
 
-#define SPRITEBATCH_IMPLEMENTATION
-#include "../../../external/cute_spritebatch.h"
-
 #include "../Misc.h"
 #include "Window/OpenGL.h"
 
@@ -40,13 +37,11 @@
 	unsigned char *pixels;
 } RenderBackend_Surface;
 
-typedef struct RenderBackend_Glyph
+typedef struct RenderBackend_GlyphAtlas
 {
-	unsigned char *pixels;
-	unsigned int width;
-	unsigned int height;
-	unsigned int pitch;
-} RenderBackend_Glyph;
+	GLuint texture_id;
+	size_t size;
+} RenderBackend_GlyphAtlas;
 
 typedef struct Coordinate2D
 {
@@ -92,8 +87,6 @@
 static unsigned char glyph_colour_channels[3];
 static RenderBackend_Surface *glyph_destination_surface;
 
-static spritebatch_t glyph_batcher;
-
 static int actual_screen_width;
 static int actual_screen_height;
 
@@ -375,156 +368,8 @@
 	current_vertex_buffer_slot = 0;
 }
 
-////////////////////
-// Glyph-batching //
-////////////////////
-
-// Blit the glyphs in the batch
-static void GlyphBatch_Draw(spritebatch_sprite_t *sprites, int count, int texture_w, int texture_h, void *udata)
-{
-	static unsigned char last_red;
-	static unsigned char last_green;
-	static unsigned char last_blue;
-
-	(void)texture_h;
-	(void)udata;
-
-	if (glyph_destination_surface == NULL)
-		return;
-
-	GLuint texture_id = (GLuint)sprites[0].texture_id;
-
-	// Flush vertex data if a context-change is needed
-	if (last_render_mode != MODE_DRAW_GLYPH || last_destination_texture != glyph_destination_surface->texture_id || last_source_texture != texture_id || last_red != glyph_colour_channels[0] || last_green != glyph_colour_channels[1] || last_blue != glyph_colour_channels[2])
-	{
-		FlushVertexBuffer();
-
-		last_render_mode = MODE_DRAW_GLYPH;
-		last_destination_texture = glyph_destination_surface->texture_id;
-		last_source_texture = texture_id;
-		last_red = glyph_colour_channels[0];
-		last_green = glyph_colour_channels[1];
-		last_blue = glyph_colour_channels[2];
-
-		glUseProgram(program_glyph);
-		glUniform4f(program_glyph_uniform_colour, glyph_colour_channels[0] / 255.0f, glyph_colour_channels[1] / 255.0f, glyph_colour_channels[2] / 255.0f, 1.0f);
-
-		// Point our framebuffer to the destination texture
-		glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, glyph_destination_surface->texture_id, 0);
-		glViewport(0, 0, glyph_destination_surface->width, glyph_destination_surface->height);
-
-		glEnable(GL_BLEND);
-
-		// Enable texture coordinates, since this uses textures
-		glEnableVertexAttribArray(ATTRIBUTE_INPUT_TEXTURE_COORDINATES);
-
-		glBindTexture(GL_TEXTURE_2D, texture_id);
-	}
-
-	// Add data to the vertex queue
-	VertexBufferSlot *vertex_buffer_slot = GetVertexBufferSlot(count);
-
-	if (vertex_buffer_slot != NULL)
-	{
-		for (int i = 0; i < count; ++i)
-		{
-			RenderBackend_Glyph *glyph = (RenderBackend_Glyph*)sprites[i].image_id;
-
-			const GLfloat texture_left = sprites[i].minx;
-			const GLfloat texture_right = texture_left + ((GLfloat)glyph->width / (GLfloat)texture_w);	// Account for width not matching pitch
-			const GLfloat texture_top = sprites[i].maxy;
-			const GLfloat texture_bottom = sprites[i].miny;
-
-			const GLfloat vertex_left = (sprites[i].x * (2.0f / glyph_destination_surface->width)) - 1.0f;
-			const GLfloat vertex_right = ((sprites[i].x + glyph->width) * (2.0f / glyph_destination_surface->width)) - 1.0f;
-			const GLfloat vertex_top = (sprites[i].y * (2.0f / glyph_destination_surface->height)) - 1.0f;
-			const GLfloat vertex_bottom = ((sprites[i].y + glyph->height) * (2.0f / glyph_destination_surface->height)) - 1.0f;
-
-			vertex_buffer_slot[i].vertices[0][0].texture.x = texture_left;
-			vertex_buffer_slot[i].vertices[0][0].texture.y = texture_top;
-			vertex_buffer_slot[i].vertices[0][1].texture.x = texture_right;
-			vertex_buffer_slot[i].vertices[0][1].texture.y = texture_top;
-			vertex_buffer_slot[i].vertices[0][2].texture.x = texture_right;
-			vertex_buffer_slot[i].vertices[0][2].texture.y = texture_bottom;
-
-			vertex_buffer_slot[i].vertices[1][0].texture.x = texture_left;
-			vertex_buffer_slot[i].vertices[1][0].texture.y = texture_top;
-			vertex_buffer_slot[i].vertices[1][1].texture.x = texture_right;
-			vertex_buffer_slot[i].vertices[1][1].texture.y = texture_bottom;
-			vertex_buffer_slot[i].vertices[1][2].texture.x = texture_left;
-			vertex_buffer_slot[i].vertices[1][2].texture.y = texture_bottom;
-
-			vertex_buffer_slot[i].vertices[0][0].position.x = vertex_left;
-			vertex_buffer_slot[i].vertices[0][0].position.y = vertex_top;
-			vertex_buffer_slot[i].vertices[0][1].position.x = vertex_right;
-			vertex_buffer_slot[i].vertices[0][1].position.y = vertex_top;
-			vertex_buffer_slot[i].vertices[0][2].position.x = vertex_right;
-			vertex_buffer_slot[i].vertices[0][2].position.y = vertex_bottom;
-
-			vertex_buffer_slot[i].vertices[1][0].position.x = vertex_left;
-			vertex_buffer_slot[i].vertices[1][0].position.y = vertex_top;
-			vertex_buffer_slot[i].vertices[1][1].position.x = vertex_right;
-			vertex_buffer_slot[i].vertices[1][1].position.y = vertex_bottom;
-			vertex_buffer_slot[i].vertices[1][2].position.x = vertex_left;
-			vertex_buffer_slot[i].vertices[1][2].position.y = vertex_bottom;
-		}
-	}
-}
-
-// Upload the glyph's pixels
-static void GlyphBatch_GetPixels(SPRITEBATCH_U64 image_id, void *buffer, int bytes_to_fill, void *udata)
-{
-	(void)udata;
-
-	RenderBackend_Glyph *glyph = (RenderBackend_Glyph*)image_id;
-
-	memcpy(buffer, glyph->pixels, bytes_to_fill);
-}
-
-// Create a texture atlas, and upload pixels to it
-static SPRITEBATCH_U64 GlyphBatch_CreateTexture(void *pixels, int w, int h, void *udata)
-{
-	(void)udata;
-
-	GLuint texture_id;
-	glGenTextures(1, &texture_id);
-	glBindTexture(GL_TEXTURE_2D, texture_id);
-#ifdef USE_OPENGLES2
-	glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, w, h, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, pixels);
-#else
-	glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, w, h, 0, GL_RED, GL_UNSIGNED_BYTE, pixels);
-#endif
-
-	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
-	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
-	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
-	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
 #ifndef USE_OPENGLES2
-	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
-#endif
 
-	glBindTexture(GL_TEXTURE_2D, last_source_texture);
-
-	return (SPRITEBATCH_U64)texture_id;
-}
-
-// Destroy texture atlas
-static void GlyphBatch_DestroyTexture(SPRITEBATCH_U64 texture_id, void *udata)
-{
-	(void)udata;
-
-	GLuint gl_texture_id = (GLuint)texture_id;
-
-	// Flush the vertex buffer if we're about to destroy its texture
-	// TODO - This leaves `last_source_texture`/`last_destination_texture` dangling
-	if (gl_texture_id == last_source_texture || gl_texture_id == last_destination_texture)
-		FlushVertexBuffer();
-
-	glDeleteTextures(1, &gl_texture_id);
-}
-
-#ifndef USE_OPENGLES2
-
 static const char* GetOpenGLErrorCodeDescription(GLenum error_code)
 {
 	switch (error_code)
@@ -656,19 +501,6 @@
 			glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, framebuffer.texture_id, 0);
 			glViewport(0, 0, framebuffer.width, framebuffer.height);
 
-			// Set-up glyph-batcher
-			spritebatch_config_t config;
-			spritebatch_set_default_config(&config);
-			config.pixel_stride = 1;
-			config.atlas_width_in_pixels = 256;
-			config.atlas_height_in_pixels = 256;
-			config.lonely_buffer_count_till_flush = 4; // Start making atlases immediately
-			config.batch_callback = GlyphBatch_Draw;
-			config.get_pixels_callback = GlyphBatch_GetPixels;
-			config.generate_texture_callback = GlyphBatch_CreateTexture;
-			config.delete_texture_callback = GlyphBatch_DestroyTexture;
-			spritebatch_init(&glyph_batcher, &config, NULL);
-
 			return &framebuffer;
 		}
 
@@ -697,8 +529,6 @@
 {
 	free(local_vertex_buffer);
 
-	spritebatch_term(&glyph_batcher);
-
 	glDeleteTextures(1, &framebuffer.texture_id);
 	glDeleteFramebuffers(1, &framebuffer_id);
 	glDeleteProgram(program_glyph);
@@ -715,8 +545,6 @@
 
 void RenderBackend_DrawScreen(void)
 {
-	spritebatch_tick(&glyph_batcher);
-
 	FlushVertexBuffer();
 	last_render_mode = MODE_BLANK;
 	last_source_texture = 0;
@@ -929,20 +757,20 @@
 	}
 
 	// Add data to the vertex queue
-	const GLfloat texture_left = (GLfloat)rect->left / (GLfloat)source_surface->width;
-	const GLfloat texture_right = (GLfloat)rect->right / (GLfloat)source_surface->width;
-	const GLfloat texture_top = (GLfloat)rect->top / (GLfloat)source_surface->height;
-	const GLfloat texture_bottom = (GLfloat)rect->bottom / (GLfloat)source_surface->height;
-
-	const GLfloat vertex_left = (x * (2.0f / destination_surface->width)) - 1.0f;
-	const GLfloat vertex_right = ((x + (rect->right - rect->left)) * (2.0f / destination_surface->width)) - 1.0f;
-	const GLfloat vertex_top = (y * (2.0f / destination_surface->height)) - 1.0f;
-	const GLfloat vertex_bottom = ((y + (rect->bottom - rect->top)) * (2.0f / destination_surface->height)) - 1.0f;
-
 	VertexBufferSlot *vertex_buffer_slot = GetVertexBufferSlot(1);
 
 	if (vertex_buffer_slot != NULL)
 	{
+		const GLfloat texture_left = (GLfloat)rect->left / (GLfloat)source_surface->width;
+		const GLfloat texture_right = (GLfloat)rect->right / (GLfloat)source_surface->width;
+		const GLfloat texture_top = (GLfloat)rect->top / (GLfloat)source_surface->height;
+		const GLfloat texture_bottom = (GLfloat)rect->bottom / (GLfloat)source_surface->height;
+
+		const GLfloat vertex_left = (x * (2.0f / destination_surface->width)) - 1.0f;
+		const GLfloat vertex_right = ((x + (rect->right - rect->left)) * (2.0f / destination_surface->width)) - 1.0f;
+		const GLfloat vertex_top = (y * (2.0f / destination_surface->height)) - 1.0f;
+		const GLfloat vertex_bottom = ((y + (rect->bottom - rect->top)) * (2.0f / destination_surface->height)) - 1.0f;
+
 		vertex_buffer_slot->vertices[0][0].texture.x = texture_left;
 		vertex_buffer_slot->vertices[0][0].texture.y = texture_top;
 		vertex_buffer_slot->vertices[0][1].texture.x = texture_right;
@@ -1009,15 +837,15 @@
 	}
 
 	// Add data to the vertex queue
-	const GLfloat vertex_left = (rect->left * (2.0f / surface->width)) - 1.0f;
-	const GLfloat vertex_right = (rect->right * (2.0f / surface->width)) - 1.0f;
-	const GLfloat vertex_top = (rect->top * (2.0f / surface->height)) - 1.0f;
-	const GLfloat vertex_bottom = (rect->bottom * (2.0f / surface->height)) - 1.0f;
-
 	VertexBufferSlot *vertex_buffer_slot = GetVertexBufferSlot(1);
 
 	if (vertex_buffer_slot != NULL)
 	{
+		const GLfloat vertex_left = (rect->left * (2.0f / surface->width)) - 1.0f;
+		const GLfloat vertex_right = (rect->right * (2.0f / surface->width)) - 1.0f;
+		const GLfloat vertex_top = (rect->top * (2.0f / surface->height)) - 1.0f;
+		const GLfloat vertex_bottom = (rect->bottom * (2.0f / surface->height)) - 1.0f;
+
 		vertex_buffer_slot->vertices[0][0].position.x = vertex_left;
 		vertex_buffer_slot->vertices[0][0].position.y = vertex_top;
 		vertex_buffer_slot->vertices[0][1].position.x = vertex_right;
@@ -1038,46 +866,62 @@
 // Glyph management //
 //////////////////////
 
-RenderBackend_Glyph* RenderBackend_LoadGlyph(const unsigned char *pixels, unsigned int width, unsigned int height, int pitch)
+RenderBackend_GlyphAtlas* RenderBackend_CreateGlyphAtlas(size_t size)
 {
-	RenderBackend_Glyph *glyph = (RenderBackend_Glyph*)malloc(sizeof(RenderBackend_Glyph));
+	RenderBackend_GlyphAtlas *atlas = (RenderBackend_GlyphAtlas*)malloc(sizeof(RenderBackend_GlyphAtlas));
 
-	if (glyph != NULL)
+	if (atlas != NULL)
 	{
-		glyph->pitch = (width + 3) & ~3;	// Round up to the nearest 4 (OpenGL needs this)
+		atlas->size = size;
 
-		glyph->pixels = (unsigned char*)malloc(glyph->pitch * height);
+		glGenTextures(1, &atlas->texture_id);
+		glBindTexture(GL_TEXTURE_2D, atlas->texture_id);
 
-		if (glyph->pixels != NULL)
-		{
-			for (unsigned int y = 0; y < height; ++y)
-			{
-				const unsigned char *source_pointer = &pixels[y * pitch];
-				unsigned char *destination_pointer = &glyph->pixels[y * glyph->pitch];
-				memcpy(destination_pointer, source_pointer, width);
-			}
+	#ifdef USE_OPENGLES2
+		glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, size, size, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, NULL);
+	#else
+		glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, size, size, 0, GL_RED, GL_UNSIGNED_BYTE, NULL);
+	#endif
 
-			glyph->width = width;
-			glyph->height = height;
+		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+	#ifndef USE_OPENGLES2
+		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
+	#endif
 
-			return glyph;
-		}
-
-		free(glyph);
+		glBindTexture(GL_TEXTURE_2D, last_source_texture);
 	}
 
-	return NULL;
+	return atlas;
 }
 
-void RenderBackend_UnloadGlyph(RenderBackend_Glyph *glyph)
+void RenderBackend_DestroyGlyphAtlas(RenderBackend_GlyphAtlas *atlas)
 {
-	if (glyph == NULL)
+	if (atlas == NULL)
 		return;
 
-	free(glyph->pixels);
-	free(glyph);
+	glDeleteTextures(1, &atlas->texture_id);
+	free(atlas);
 }
 
+void RenderBackend_UploadGlyph(RenderBackend_GlyphAtlas *atlas, size_t x, size_t y, const unsigned char *pixels, size_t width, size_t height)
+{
+	if (atlas == NULL)
+		return;
+
+	glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
+	glBindTexture(GL_TEXTURE_2D, atlas->texture_id);
+#ifdef USE_OPENGLES2
+	glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, width, height, GL_LUMINANCE, GL_UNSIGNED_BYTE, pixels);
+#else
+	glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, width, height, GL_RED, GL_UNSIGNED_BYTE, pixels);
+#endif
+	glBindTexture(GL_TEXTURE_2D, last_source_texture);
+	glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
+}
+
 void RenderBackend_PrepareToDrawGlyphs(RenderBackend_Surface *destination_surface, const unsigned char *colour_channels)
 {
 	glyph_destination_surface = destination_surface;
@@ -1085,15 +929,90 @@
 	memcpy(glyph_colour_channels, colour_channels, sizeof(glyph_colour_channels));
 }
 
-void RenderBackend_DrawGlyph(RenderBackend_Glyph *glyph, long x, long y)
+void RenderBackend_DrawGlyph(RenderBackend_GlyphAtlas *atlas, long x, long y, size_t glyph_x, size_t glyph_y, size_t glyph_width, size_t glyph_height)
 {
-	spritebatch_push(&glyph_batcher, (SPRITEBATCH_U64)glyph, glyph->pitch, glyph->height, x, y, 1.0f, 1.0f, 0.0f, 0.0f, 0);
+	static unsigned char last_red;
+	static unsigned char last_green;
+	static unsigned char last_blue;
+
+	if (glyph_destination_surface == NULL)
+		return;
+
+	// Flush vertex data if a context-change is needed
+	if (last_render_mode != MODE_DRAW_GLYPH || last_destination_texture != glyph_destination_surface->texture_id || last_source_texture != atlas->texture_id || last_red != glyph_colour_channels[0] || last_green != glyph_colour_channels[1] || last_blue != glyph_colour_channels[2])
+	{
+		FlushVertexBuffer();
+
+		last_render_mode = MODE_DRAW_GLYPH;
+		last_destination_texture = glyph_destination_surface->texture_id;
+		last_source_texture = atlas->texture_id;
+		last_red = glyph_colour_channels[0];
+		last_green = glyph_colour_channels[1];
+		last_blue = glyph_colour_channels[2];
+
+		glUseProgram(program_glyph);
+		glUniform4f(program_glyph_uniform_colour, glyph_colour_channels[0] / 255.0f, glyph_colour_channels[1] / 255.0f, glyph_colour_channels[2] / 255.0f, 1.0f);
+
+		// Point our framebuffer to the destination texture
+		glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, glyph_destination_surface->texture_id, 0);
+		glViewport(0, 0, glyph_destination_surface->width, glyph_destination_surface->height);
+
+		glEnable(GL_BLEND);
+
+		// Enable texture coordinates, since this uses textures
+		glEnableVertexAttribArray(ATTRIBUTE_INPUT_TEXTURE_COORDINATES);
+
+		glBindTexture(GL_TEXTURE_2D, atlas->texture_id);
+	}
+
+	// Add data to the vertex queue
+	VertexBufferSlot *vertex_buffer_slot = GetVertexBufferSlot(1);
+
+	if (vertex_buffer_slot != NULL)
+	{
+		const GLfloat texture_left = glyph_x / (GLfloat)atlas->size;
+		const GLfloat texture_right = (glyph_x + glyph_width) / (GLfloat)atlas->size;
+		const GLfloat texture_top = glyph_y / (GLfloat)atlas->size;
+		const GLfloat texture_bottom = (glyph_y + glyph_height) / (GLfloat)atlas->size;
+
+		const GLfloat vertex_left = (x * (2.0f / glyph_destination_surface->width)) - 1.0f;
+		const GLfloat vertex_right = ((x + glyph_width) * (2.0f / glyph_destination_surface->width)) - 1.0f;
+		const GLfloat vertex_top = (y * (2.0f / glyph_destination_surface->height)) - 1.0f;
+		const GLfloat vertex_bottom = ((y + glyph_height) * (2.0f / glyph_destination_surface->height)) - 1.0f;
+
+		vertex_buffer_slot->vertices[0][0].texture.x = texture_left;
+		vertex_buffer_slot->vertices[0][0].texture.y = texture_top;
+		vertex_buffer_slot->vertices[0][1].texture.x = texture_right;
+		vertex_buffer_slot->vertices[0][1].texture.y = texture_top;
+		vertex_buffer_slot->vertices[0][2].texture.x = texture_right;
+		vertex_buffer_slot->vertices[0][2].texture.y = texture_bottom;
+
+		vertex_buffer_slot->vertices[1][0].texture.x = texture_left;
+		vertex_buffer_slot->vertices[1][0].texture.y = texture_top;
+		vertex_buffer_slot->vertices[1][1].texture.x = texture_right;
+		vertex_buffer_slot->vertices[1][1].texture.y = texture_bottom;
+		vertex_buffer_slot->vertices[1][2].texture.x = texture_left;
+		vertex_buffer_slot->vertices[1][2].texture.y = texture_bottom;
+
+		vertex_buffer_slot->vertices[0][0].position.x = vertex_left;
+		vertex_buffer_slot->vertices[0][0].position.y = vertex_top;
+		vertex_buffer_slot->vertices[0][1].position.x = vertex_right;
+		vertex_buffer_slot->vertices[0][1].position.y = vertex_top;
+		vertex_buffer_slot->vertices[0][2].position.x = vertex_right;
+		vertex_buffer_slot->vertices[0][2].position.y = vertex_bottom;
+
+		vertex_buffer_slot->vertices[1][0].position.x = vertex_left;
+		vertex_buffer_slot->vertices[1][0].position.y = vertex_top;
+		vertex_buffer_slot->vertices[1][1].position.x = vertex_right;
+		vertex_buffer_slot->vertices[1][1].position.y = vertex_bottom;
+		vertex_buffer_slot->vertices[1][2].position.x = vertex_left;
+		vertex_buffer_slot->vertices[1][2].position.y = vertex_bottom;
+	}
 }
 
 void RenderBackend_FlushGlyphs(void)
 {
-	spritebatch_defrag(&glyph_batcher);
-	spritebatch_flush(&glyph_batcher);
+	
 }
 
 ///////////
--- a/src/Backends/Rendering/Software.cpp
+++ b/src/Backends/Rendering/Software.cpp
@@ -19,12 +19,11 @@
 	unsigned int pitch;
 } RenderBackend_Surface;
 
-typedef struct RenderBackend_Glyph
+typedef struct RenderBackend_GlyphAtlas
 {
 	unsigned char *pixels;
-	unsigned int width;
-	unsigned int height;
-} RenderBackend_Glyph;
+	size_t size;
+} RenderBackend_GlyphAtlas;
 
 static RenderBackend_Surface framebuffer;
 
@@ -273,37 +272,40 @@
 	}
 }
 
-RenderBackend_Glyph* RenderBackend_LoadGlyph(const unsigned char *pixels, unsigned int width, unsigned int height, int pitch)
+RenderBackend_GlyphAtlas* RenderBackend_CreateGlyphAtlas(size_t size)
 {
-	RenderBackend_Glyph *glyph = (RenderBackend_Glyph*)malloc(sizeof(RenderBackend_Glyph));
+	RenderBackend_GlyphAtlas *atlas = (RenderBackend_GlyphAtlas*)malloc(sizeof(RenderBackend_GlyphAtlas));
 
-	if (glyph == NULL)
-		return NULL;
+	if (atlas != NULL)
+	{
+		atlas->pixels = (unsigned char*)malloc(size * size);
 
-	glyph->pixels = (unsigned char*)malloc(width * height);
+		if (atlas->pixels != NULL)
+		{
+			atlas->size = size;
 
-	if (glyph->pixels == NULL)
-	{
-		free(glyph);
-		return NULL;
+			return atlas;
+		}
+
+		free(atlas);
 	}
 
-	for (unsigned int y = 0; y < height; ++y)
-		memcpy(&glyph->pixels[y * width], &pixels[y * pitch], width);
+	return NULL;	
+}
 
-	glyph->width = width;
-	glyph->height = height;
-
-	return glyph;
+void RenderBackend_DestroyGlyphAtlas(RenderBackend_GlyphAtlas *atlas)
+{
+	free(atlas->pixels);
+	free(atlas);
 }
 
-void RenderBackend_UnloadGlyph(RenderBackend_Glyph *glyph)
+void RenderBackend_UploadGlyph(RenderBackend_GlyphAtlas *atlas, size_t x, size_t y, const unsigned char *pixels, size_t width, size_t height)
 {
-	if (glyph == NULL)
-		return;
+	if (atlas == NULL)
+	 return;
 
-	free(glyph->pixels);
-	free(glyph);
+	for (size_t i = 0; i < height; ++i)
+		memcpy(&atlas->pixels[(y + i) * atlas->size + x], &pixels[i * width], width);
 }
 
 void RenderBackend_PrepareToDrawGlyphs(RenderBackend_Surface *destination_surface, const unsigned char *colour_channels)
@@ -316,16 +318,16 @@
 	memcpy(glyph_colour_channels, colour_channels, sizeof(glyph_colour_channels));
 }
 
-void RenderBackend_DrawGlyph(RenderBackend_Glyph *glyph, long x, long y)
+void RenderBackend_DrawGlyph(RenderBackend_GlyphAtlas *atlas, long x, long y, size_t glyph_x, size_t glyph_y, size_t glyph_width, size_t glyph_height)
 {
-	if (glyph == NULL)
+	if (atlas == NULL)
 		return;
 
-	for (unsigned int iy = MAX(-y, 0); y + iy < MIN(y + glyph->height, glyph_destination_surface->height); ++iy)
+	for (unsigned int iy = MAX(-y, 0); y + iy < MIN(y + glyph_height, glyph_destination_surface->height); ++iy)
 	{
-		for (unsigned int ix = MAX(-x, 0); x + ix < MIN(x + glyph->width, glyph_destination_surface->width); ++ix)
+		for (unsigned int ix = MAX(-x, 0); x + ix < MIN(x + glyph_width, glyph_destination_surface->width); ++ix)
 		{
-			const unsigned char alpha_int = glyph->pixels[iy * glyph->width + ix];
+			const unsigned char alpha_int = atlas->pixels[(glyph_y + iy) * atlas->size + (glyph_x + ix)];
 
 			if (alpha_int != 0)
 			{
--- a/src/Font.cpp
+++ b/src/Font.cpp
@@ -3,6 +3,7 @@
 #include <stddef.h>
 #include <stdlib.h>
 #include <string.h>
+#include <math.h>
 
 #include <ft2build.h>
 #include FT_FREETYPE_H
@@ -21,16 +22,26 @@
 // If you'd like the buggy anti-aliasing, then uncomment the following line.
 //#define ENABLE_FONT_ANTIALIASING
 
-typedef struct CachedGlyph
+// This controls however many glyphs (letters) the game can cache in VRAM at once
+#define TOTAL_GLYPH_SLOTS 128
+
+typedef struct Glyph
 {
 	unsigned long unicode_value;
+
 	int x;
 	int y;
+
+	int width;
+	int height;
+
+	int x_offset;
+	int y_offset;
+
 	int x_advance;
-	RenderBackend_Glyph *backend;
 
-	struct CachedGlyph *next;
-} CachedGlyph;
+	struct Glyph *next;
+} Glyph;
 
 typedef struct FontObject
 {
@@ -37,7 +48,10 @@
 	FT_Library library;
 	FT_Face face;
 	unsigned char *data;
-	CachedGlyph *glyph_list_head;
+	Glyph glyphs[TOTAL_GLYPH_SLOTS];
+	Glyph *glyph_list_head;
+	RenderBackend_GlyphAtlas *atlas;
+	size_t atlas_row_length;
 } FontObject;
 
 #ifdef JAPANESE
@@ -946,108 +960,104 @@
 	return lookup[value];
 }
 
-static CachedGlyph* GetGlyphCached(FontObject *font_object, unsigned long unicode_value)
+
+static Glyph* GetGlyph(FontObject *font_object, unsigned long unicode_value)
 {
-	CachedGlyph *glyph;
+	Glyph **glyph_pointer = &font_object->glyph_list_head;
 
-	for (glyph = font_object->glyph_list_head; glyph != NULL; glyph = glyph->next)
+	for (;;)
+	{
+		Glyph *glyph = *glyph_pointer;
+
 		if (glyph->unicode_value == unicode_value)
+		{
+			// Move it to the front of the list
+			*glyph_pointer = glyph->next;
+			glyph->next = font_object->glyph_list_head;
+			font_object->glyph_list_head = glyph;
+
 			return glyph;
+		}
 
-	glyph = (CachedGlyph*)malloc(sizeof(CachedGlyph));
+		if (glyph->next == NULL)
+			break;
 
-	if (glyph != NULL)
-	{
-		unsigned int glyph_index = FT_Get_Char_Index(font_object->face, unicode_value);
+		glyph_pointer = &glyph->next;
+	}
 
+	Glyph *glyph = *glyph_pointer;
+
+	// Couldn't find glyph - overwrite the old at the end.
+	// The one at the end hasn't been used in a while anyway.
+
+	unsigned int glyph_index = FT_Get_Char_Index(font_object->face, unicode_value);
+
 #ifdef ENABLE_FONT_ANTIALIASING
-		if (FT_Load_Glyph(font_object->face, glyph_index, FT_LOAD_RENDER) == 0)
+	if (FT_Load_Glyph(font_object->face, glyph_index, FT_LOAD_RENDER) == 0)
 #else
-		if (FT_Load_Glyph(font_object->face, glyph_index, FT_LOAD_RENDER | FT_LOAD_TARGET_MONO) == 0)
+	if (FT_Load_Glyph(font_object->face, glyph_index, FT_LOAD_RENDER | FT_LOAD_TARGET_MONO) == 0)
 #endif
-		{
-			glyph->unicode_value = unicode_value;
-			glyph->x = font_object->face->glyph->bitmap_left;
-			glyph->y = (font_object->face->size->metrics.ascender + (64 / 2)) / 64 - font_object->face->glyph->bitmap_top;
-			glyph->x_advance = font_object->face->glyph->advance.x / 64;
+	{
+		FT_Bitmap bitmap;
+		FT_Bitmap_New(&bitmap);
 
-			FT_Bitmap bitmap;
-			FT_Bitmap_New(&bitmap);
-
-			if (FT_Bitmap_Convert(font_object->library, &font_object->face->glyph->bitmap, &bitmap, 1) == 0)
+		if (FT_Bitmap_Convert(font_object->library, &font_object->face->glyph->bitmap, &bitmap, 1) == 0)
+		{
+			switch (font_object->face->glyph->bitmap.pixel_mode)
 			{
-				switch (font_object->face->glyph->bitmap.pixel_mode)
-				{
-					case FT_PIXEL_MODE_GRAY:
-						for (unsigned int y = 0; y < bitmap.rows; ++y)
-						{
-							unsigned char *pixel_pointer = bitmap.buffer + y * bitmap.pitch;
+				case FT_PIXEL_MODE_GRAY:
+					for (unsigned int y = 0; y < bitmap.rows; ++y)
+					{
+						unsigned char *pixel_pointer = &bitmap.buffer[y * bitmap.pitch];
 
-							for (unsigned int x = 0; x < bitmap.width; ++x)
-							{
-								*pixel_pointer = GammaCorrect((*pixel_pointer * 0xFF) / (bitmap.num_grays - 1));
-								++pixel_pointer;
-							}
+						for (unsigned int x = 0; x < bitmap.width; ++x)
+						{
+							*pixel_pointer = GammaCorrect((*pixel_pointer * 0xFF) / (bitmap.num_grays - 1));
+							++pixel_pointer;
 						}
+					}
 
-						break;
+					break;
 
-					case FT_PIXEL_MODE_MONO:
-						for (unsigned int y = 0; y < bitmap.rows; ++y)
-						{
-							unsigned char *pixel_pointer = bitmap.buffer + y * bitmap.pitch;
+				case FT_PIXEL_MODE_MONO:
+					for (unsigned int y = 0; y < bitmap.rows; ++y)
+					{
+						unsigned char *pixel_pointer = &bitmap.buffer[y * bitmap.pitch];
 
-							for (unsigned int x = 0; x < bitmap.width; ++x)
-							{
-								*pixel_pointer = *pixel_pointer ? 0xFF : 0;
-								++pixel_pointer;
-							}
+						for (unsigned int x = 0; x < bitmap.width; ++x)
+						{
+							*pixel_pointer = *pixel_pointer ? 0xFF : 0;
+							++pixel_pointer;
 						}
+					}
 
-						break;
-				}
+					break;
+			}
 
-				// Don't bother loading glyphs that don't have an image (causes `cute_spritebatch.h` to freak-out)
-				if (bitmap.width == 0 || bitmap.rows == 0)
-					glyph->backend = NULL;
-				else
-					glyph->backend = RenderBackend_LoadGlyph(bitmap.buffer, bitmap.width, bitmap.rows, bitmap.pitch);
+			glyph->unicode_value = unicode_value;
+			glyph->width = bitmap.width;
+			glyph->height = bitmap.rows;
+			glyph->x_offset = font_object->face->glyph->bitmap_left;
+			glyph->y_offset = (font_object->face->size->metrics.ascender + (64 / 2)) / 64 - font_object->face->glyph->bitmap_top;
+			glyph->x_advance = font_object->face->glyph->advance.x / 64;
 
-				FT_Bitmap_Done(font_object->library, &bitmap);
+			RenderBackend_UploadGlyph(font_object->atlas, glyph->x, glyph->y, bitmap.buffer, glyph->width, glyph->height);
 
-				glyph->next = font_object->glyph_list_head;
-				font_object->glyph_list_head = glyph;
+			FT_Bitmap_Done(font_object->library, &bitmap);
 
-				return glyph;
-			}
+			*glyph_pointer = glyph->next;
+			glyph->next = font_object->glyph_list_head;
+			font_object->glyph_list_head = glyph;
 
-			FT_Bitmap_Done(font_object->library, &bitmap);
+			return glyph;
 		}
 
-		free(glyph);
+		FT_Bitmap_Done(font_object->library, &bitmap);
 	}
 
 	return NULL;
 }
 
-static void UnloadCachedGlyphs(FontObject *font_object)
-{
-	CachedGlyph *glyph = font_object->glyph_list_head;
-	while (glyph != NULL)
-	{
-		CachedGlyph *next_glyph = glyph->next;
-
-		if (glyph->backend != NULL)
-			RenderBackend_UnloadGlyph(glyph->backend);
-
-		free(glyph);
-
-		glyph = next_glyph;
-	}
-
-	font_object->glyph_list_head = NULL;
-}
-
 FontObject* LoadFontFromData(const unsigned char *data, size_t data_size, unsigned int cell_width, unsigned int cell_height)
 {
 	FontObject *font_object = (FontObject*)malloc(sizeof(FontObject));
@@ -1068,7 +1078,33 @@
 
 					font_object->glyph_list_head = NULL;
 
-					return font_object;
+					size_t atlas_entry_width = (font_object->face->bbox.xMax - font_object->face->bbox.xMin) >> 6;
+					size_t atlas_entry_height = (font_object->face->bbox.yMax - font_object->face->bbox.yMin) >> 6;
+
+					font_object->atlas_row_length = ceil(sqrt(atlas_entry_width * atlas_entry_height * TOTAL_GLYPH_SLOTS) / atlas_entry_width);
+
+					size_t texture_size = font_object->atlas_row_length * atlas_entry_width;
+
+					font_object->atlas = RenderBackend_CreateGlyphAtlas(texture_size);
+
+					if (font_object->atlas != NULL)
+					{
+						// Initialise the linked-list
+						for (size_t i = 0; i < TOTAL_GLYPH_SLOTS; ++i)
+						{
+							font_object->glyphs[i].x = (i % font_object->atlas_row_length) * atlas_entry_width;
+							font_object->glyphs[i].y = (i / font_object->atlas_row_length) * atlas_entry_height;
+
+							if (i != TOTAL_GLYPH_SLOTS - 1)
+								font_object->glyphs[i].next = &font_object->glyphs[i + 1];
+						}
+
+						font_object->glyph_list_head = font_object->glyphs;
+
+						return font_object;
+					}
+
+					FT_Done_Face(font_object->face);
 				}
 
 				free(font_object->data);
@@ -1115,22 +1151,21 @@
 		while (string_pointer != string_end)
 		{
 			unsigned int bytes_read;
-#ifdef JAPANESE
+		#ifdef JAPANESE
 			const unsigned short unicode_value = ShiftJISToUnicode(string_pointer, &bytes_read);
-#else
+		#else
 			const unsigned long unicode_value = UTF8ToUnicode(string_pointer, &bytes_read);
-#endif
+		#endif
 			string_pointer += bytes_read;
 
-			CachedGlyph *glyph = GetGlyphCached(font_object, unicode_value);
+			Glyph *glyph = GetGlyph(font_object, unicode_value);
 
 			if (glyph != NULL)
 			{
-				const int letter_x = x + pen_x + glyph->x;
-				const int letter_y = y + glyph->y;
+				const long letter_x = x + pen_x + glyph->x_offset;
+				const long letter_y = y + glyph->y_offset;
 
-				if (glyph->backend != NULL)
-					RenderBackend_DrawGlyph(glyph->backend, letter_x, letter_y);
+				RenderBackend_DrawGlyph(font_object->atlas, letter_x, letter_y, glyph->x, glyph->y, glyph->width, glyph->height);
 
 				pen_x += glyph->x_advance;
 			}
@@ -1144,7 +1179,7 @@
 {
 	if (font_object != NULL)
 	{
-		UnloadCachedGlyphs(font_object);
+		RenderBackend_DestroyGlyphAtlas(font_object->atlas);
 
 		FT_Done_Face(font_object->face);
 		free(font_object->data);