From 096fa3b578a05687814abdc65107bf05fadaeea7 Mon Sep 17 00:00:00 2001 From: Clownacy Date: Mon, 7 Sep 2020 23:11:01 +0100 Subject: [PATCH] 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. --- src/Backends/Rendering.h | 11 +- src/Backends/Rendering/OpenGL3.cpp | 357 +++++++++++----------------- src/Backends/Rendering/Software.cpp | 64 ++--- src/Font.cpp | 245 +++++++++++-------- 4 files changed, 318 insertions(+), 359 deletions(-) diff --git a/src/Backends/Rendering.h b/src/Backends/Rendering.h index f1cac7f7..74866adb 100644 --- a/src/Backends/Rendering.h +++ b/src/Backends/Rendering.h @@ -1,7 +1,9 @@ #pragma once +#include + 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); diff --git a/src/Backends/Rendering/OpenGL3.cpp b/src/Backends/Rendering/OpenGL3.cpp index 43593741..a8f5fb79 100644 --- a/src/Backends/Rendering/OpenGL3.cpp +++ b/src/Backends/Rendering/OpenGL3.cpp @@ -12,9 +12,6 @@ #include #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,20 +757,20 @@ void RenderBackend_Blit(RenderBackend_Surface *source_surface, const RenderBacke } // 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 @@ void RenderBackend_ColourFill(RenderBackend_Surface *surface, const RenderBacken } // 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,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 - 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) @@ -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); + } /////////// diff --git a/src/Backends/Rendering/Software.cpp b/src/Backends/Rendering/Software.cpp index 2b522d63..a3e9237f 100644 --- a/src/Backends/Rendering/Software.cpp +++ b/src/Backends/Rendering/Software.cpp @@ -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); - return NULL; + atlas->pixels = (unsigned char*)malloc(size * size); + + if (atlas->pixels != NULL) + { + atlas->size = size; + + return atlas; + } + + free(atlas); } - 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; + return NULL; } -void RenderBackend_UnloadGlyph(RenderBackend_Glyph *glyph) +void RenderBackend_DestroyGlyphAtlas(RenderBackend_GlyphAtlas *atlas) { - if (glyph == NULL) - return; + free(atlas->pixels); + free(atlas); +} - free(glyph->pixels); - free(glyph); +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; + + 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) { diff --git a/src/Font.cpp b/src/Font.cpp index 929b6d5a..fe22b7b5 100644 --- a/src/Font.cpp +++ b/src/Font.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #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,108 +960,104 @@ static unsigned char GammaCorrect(unsigned char value) 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) - if (glyph->unicode_value == unicode_value) - return glyph; - - glyph = (CachedGlyph*)malloc(sizeof(CachedGlyph)); - - if (glyph != NULL) + for (;;) { - unsigned int glyph_index = FT_Get_Char_Index(font_object->face, unicode_value); + Glyph *glyph = *glyph_pointer; -#ifdef ENABLE_FONT_ANTIALIASING - 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) -#endif + if (glyph->unicode_value == unicode_value) { - 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; + // 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; - FT_Bitmap bitmap; - FT_Bitmap_New(&bitmap); - - if (FT_Bitmap_Convert(font_object->library, &font_object->face->glyph->bitmap, &bitmap, 1) == 0) - { - 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; - - for (unsigned int x = 0; x < bitmap.width; ++x) - { - *pixel_pointer = GammaCorrect((*pixel_pointer * 0xFF) / (bitmap.num_grays - 1)); - ++pixel_pointer; - } - } - - break; - - 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; - } - } - - 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); - - FT_Bitmap_Done(font_object->library, &bitmap); - - glyph->next = font_object->glyph_list_head; - font_object->glyph_list_head = glyph; - - return glyph; - } - - FT_Bitmap_Done(font_object->library, &bitmap); + return glyph; } - free(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 + 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) +#endif + { + FT_Bitmap bitmap; + FT_Bitmap_New(&bitmap); + + if (FT_Bitmap_Convert(font_object->library, &font_object->face->glyph->bitmap, &bitmap, 1) == 0) + { + 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]; + + for (unsigned int x = 0; x < bitmap.width; ++x) + { + *pixel_pointer = GammaCorrect((*pixel_pointer * 0xFF) / (bitmap.num_grays - 1)); + ++pixel_pointer; + } + } + + break; + + 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; + } + } + + break; + } + + 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; + + return 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 @@ FontObject* LoadFontFromData(const unsigned char *data, size_t data_size, unsign 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 @@ void DrawText(FontObject *font_object, RenderBackend_Surface *surface, int x, in 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 @@ 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);