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:
parent
1dcf3333fc
commit
096fa3b578
4 changed files with 318 additions and 359 deletions
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
///////////
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
143
src/Font.cpp
143
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,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);
|
||||
|
|
Loading…
Add table
Reference in a new issue