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.
This commit is contained in:
Clownacy 2020-09-07 23:11:01 +01:00
parent 1dcf3333fc
commit 096fa3b578
4 changed files with 318 additions and 359 deletions

View file

@ -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 @@ unsigned char* RenderBackend_LockSurface(RenderBackend_Surface *surface, unsigne
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);

View file

@ -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 @@ typedef struct RenderBackend_Surface
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 RenderBackend_Surface framebuffer;
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,154 +368,6 @@ static void FlushVertexBuffer(void)
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)
@ -656,19 +501,6 @@ RenderBackend_Surface* RenderBackend_Init(const char *window_title, int screen_w
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 @@ void RenderBackend_Deinit(void)
{
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_Deinit(void)
void RenderBackend_DrawScreen(void)
{
spritebatch_tick(&glyph_batcher);
FlushVertexBuffer();
last_render_mode = MODE_BLANK;
last_source_texture = 0;
@ -929,6 +757,10 @@ void RenderBackend_Blit(RenderBackend_Surface *source_surface, const RenderBacke
}
// Add data to the vertex queue
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;
@ -939,10 +771,6 @@ void RenderBackend_Blit(RenderBackend_Surface *source_surface, const RenderBacke
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)
{
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 @@ void RenderBackend_ColourFill(RenderBackend_Surface *surface, const RenderBacken
}
// Add data to the vertex queue
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;
VertexBufferSlot *vertex_buffer_slot = GetVertexBufferSlot(1);
if (vertex_buffer_slot != NULL)
{
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,44 +866,60 @@ void RenderBackend_ColourFill(RenderBackend_Surface *surface, const RenderBacken
// 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
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);
}
glyph->width = width;
glyph->height = height;
return glyph;
return atlas;
}
free(glyph);
}
return NULL;
}
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)
@ -1085,15 +929,90 @@ void RenderBackend_PrepareToDrawGlyphs(RenderBackend_Surface *destination_surfac
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);
}
///////////

View file

@ -19,12 +19,11 @@ typedef struct RenderBackend_Surface
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 @@ ATTRIBUTE_HOT void RenderBackend_ColourFill(RenderBackend_Surface *surface, cons
}
}
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;
glyph->pixels = (unsigned char*)malloc(width * height);
if (glyph->pixels == NULL)
if (atlas != NULL)
{
free(glyph);
atlas->pixels = (unsigned char*)malloc(size * size);
if (atlas->pixels != NULL)
{
atlas->size = size;
return atlas;
}
free(atlas);
}
return NULL;
}
for (unsigned int y = 0; y < height; ++y)
memcpy(&glyph->pixels[y * width], &pixels[y * pitch], width);
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)
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 @@ void RenderBackend_PrepareToDrawGlyphs(RenderBackend_Surface *destination_surfac
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)
{

View file

@ -3,6 +3,7 @@
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <ft2build.h>
#include FT_FREETYPE_H
@ -21,23 +22,36 @@
// 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 x_advance;
RenderBackend_Glyph *backend;
struct CachedGlyph *next;
} CachedGlyph;
int width;
int height;
int x_offset;
int y_offset;
int x_advance;
struct Glyph *next;
} Glyph;
typedef struct FontObject
{
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,18 +960,36 @@ static unsigned char GammaCorrect(unsigned char value)
return lookup[value];
}
static CachedGlyph* GetGlyphCached(FontObject *font_object, unsigned long unicode_value)
{
CachedGlyph *glyph;
for (glyph = font_object->glyph_list_head; glyph != NULL; glyph = glyph->next)
static Glyph* GetGlyph(FontObject *font_object, unsigned long unicode_value)
{
Glyph **glyph_pointer = &font_object->glyph_list_head;
for (;;)
{
Glyph *glyph = *glyph_pointer;
if (glyph->unicode_value == unicode_value)
return glyph;
glyph = (CachedGlyph*)malloc(sizeof(CachedGlyph));
if (glyph != NULL)
{
// 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;
}
if (glyph->next == NULL)
break;
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
@ -966,11 +998,6 @@ static CachedGlyph* GetGlyphCached(FontObject *font_object, unsigned long unicod
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);
@ -981,7 +1008,7 @@ static CachedGlyph* GetGlyphCached(FontObject *font_object, unsigned long unicod
case FT_PIXEL_MODE_GRAY:
for (unsigned int y = 0; y < bitmap.rows; ++y)
{
unsigned char *pixel_pointer = bitmap.buffer + y * bitmap.pitch;
unsigned char *pixel_pointer = &bitmap.buffer[y * bitmap.pitch];
for (unsigned int x = 0; x < bitmap.width; ++x)
{
@ -995,7 +1022,7 @@ static CachedGlyph* GetGlyphCached(FontObject *font_object, unsigned long unicod
case FT_PIXEL_MODE_MONO:
for (unsigned int y = 0; y < bitmap.rows; ++y)
{
unsigned char *pixel_pointer = bitmap.buffer + y * bitmap.pitch;
unsigned char *pixel_pointer = &bitmap.buffer[y * bitmap.pitch];
for (unsigned int x = 0; x < bitmap.width; ++x)
{
@ -1007,14 +1034,18 @@ static CachedGlyph* GetGlyphCached(FontObject *font_object, unsigned long unicod
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;
RenderBackend_UploadGlyph(font_object->atlas, glyph->x, glyph->y, bitmap.buffer, glyph->width, glyph->height);
FT_Bitmap_Done(font_object->library, &bitmap);
*glyph_pointer = glyph->next;
glyph->next = font_object->glyph_list_head;
font_object->glyph_list_head = glyph;
@ -1024,30 +1055,9 @@ static CachedGlyph* GetGlyphCached(FontObject *font_object, unsigned long unicod
FT_Bitmap_Done(font_object->library, &bitmap);
}
free(glyph);
}
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,9 +1078,35 @@ FontObject* LoadFontFromData(const unsigned char *data, size_t data_size, unsign
font_object->glyph_list_head = NULL;
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);
}
@ -1122,15 +1158,14 @@ void DrawText(FontObject *font_object, RenderBackend_Surface *surface, int x, in
#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 @@ void UnloadFont(FontObject *font_object)
{
if (font_object != NULL)
{
UnloadCachedGlyphs(font_object);
RenderBackend_DestroyGlyphAtlas(font_object->atlas);
FT_Done_Face(font_object->face);
free(font_object->data);