#include "twn_draw_c.h" #include "twn_draw.h" #include "twn_util.h" #include "twn_util_c.h" #include "twn_engine_context_c.h" #include #include #define ASCII_START 32 #define ASCII_END 128 #define NUM_DISPLAY_ASCII ((ASCII_END - ASCII_START) + 1) /* should be large enough that there's virtually never more than one block */ #define STRING_ARENA_BLOCK_SIZE 512000 typedef struct StringArenaBlock { struct StringArenaBlock *next; char *buffer; size_t used; } StringArenaBlock; typedef struct StringArena { StringArenaBlock *head; StringArenaBlock *current_block; char *ptr; } StringArena; static StringArena string_arena; typedef struct FontFileBuffer { size_t len; unsigned char *buffer; } FontFileBuffer; typedef struct FontFileCacheItem { char *key; FontFileBuffer value; } FontFileCacheItem; static FontFileCacheItem *font_file_cache_hash; static void string_arena_init(StringArena *arena) { arena->head = cmalloc(sizeof *arena->head); arena->current_block = arena->head; arena->ptr = arena->current_block->buffer; arena->current_block->next = NULL; arena->current_block->buffer = cmalloc(STRING_ARENA_BLOCK_SIZE); arena->current_block->used = 0; } static void string_arena_deinit(StringArena *arena) { StringArenaBlock *p = arena->head; while (p != NULL) { StringArenaBlock *block = p; p = block->next; /* need to keep this before freeing */ SDL_free(block->buffer); SDL_free(block); } } static void string_arena_reset(StringArena *arena) { arena->current_block = arena->head; arena->current_block->used = 0; arena->ptr = arena->current_block->buffer; } static char *string_arena_alloc(StringArena *arena, size_t size) { /* will never fit! */ if (size > STRING_ARENA_BLOCK_SIZE) { /* is there a more graceful way to handle this without risking some sort of horror? */ CRY("String arena allocation failure", "Tried to allocate a size larger than the block size"); die_abruptly(); } /* won't fit, we need a new block first */ if (arena->current_block->used + size > STRING_ARENA_BLOCK_SIZE) { log_info("String arena block size exceeded, moving on to the next..."); /* only allocate if there wasn't one ready to go already */ if (arena->current_block->next == NULL) { log_info("Allocating string arena block..."); arena->current_block->next = cmalloc(sizeof *arena->head); arena->current_block->next->next = NULL; arena->current_block->next->buffer = cmalloc(STRING_ARENA_BLOCK_SIZE); } arena->current_block = arena->current_block->next; arena->current_block->used = 0; arena->ptr = arena->current_block->buffer; } char *chunk = arena->ptr; arena->ptr += size; arena->current_block->used += size * (sizeof *arena->ptr); return chunk; } static FontData *text_load_font_data(const char *path, int height_px) { FontData *font_data; unsigned char* bitmap; { unsigned char *buf = NULL; int64_t buf_len = 0; /* if the file was already loaded just get it */ FontFileCacheItem *font_file_ptr = shgetp_null(font_file_cache_hash, path); if (font_file_ptr != NULL) { buf = font_file_ptr->value.buffer; buf_len = font_file_ptr->value.len; } else { buf_len = file_to_bytes(path, &buf); if (buf_len == -1) { /* TODO: have a fallback default font */ log_warn("Font %s not found", path); return NULL; } FontFileBuffer buffer = { buf_len, buf }; shput(font_file_cache_hash, path, buffer); } font_data = ccalloc(1, sizeof *font_data); font_data->file_path = path; font_data->height_px = height_px; bitmap = ccalloc(ctx.font_texture_size * ctx.font_texture_size, 1); stbtt_InitFont(&font_data->info, buf, stbtt_GetFontOffsetForIndex(buf, 0)); /* might as well get these now, for later */ font_data->scale_factor = stbtt_ScaleForPixelHeight(&font_data->info, (float)height_px); stbtt_GetFontVMetrics( &font_data->info, &font_data->ascent, &font_data->descent, &font_data->line_gap ); font_data->ascent = (int)((float)font_data->ascent * font_data->scale_factor); font_data->descent = (int)((float)font_data->descent * font_data->scale_factor); font_data->line_gap = (int)((float)font_data->line_gap * font_data->scale_factor); stbtt_pack_context pctx; stbtt_PackBegin(&pctx, bitmap, (int)ctx.font_texture_size, (int)ctx.font_texture_size, 0, 1, NULL); stbtt_PackSetOversampling(&pctx, (unsigned int)ctx.font_oversampling, (unsigned int)ctx.font_oversampling); stbtt_PackFontRange(&pctx, buf, 0, (float)height_px, ASCII_START, NUM_DISPLAY_ASCII, font_data->char_data); stbtt_PackEnd(&pctx); } font_data->texture = create_gpu_texture(ctx.font_filtering, true, 1, (int)ctx.font_texture_size, (int)ctx.font_texture_size); upload_gpu_texture( font_data->texture, bitmap, 1, (int)ctx.font_texture_size, (int)ctx.font_texture_size ); SDL_free(bitmap); return font_data; } static void text_destroy_font_data(FontData *font_data) { delete_gpu_texture(font_data->texture); SDL_free(font_data); } static void text_draw_with(FontData* font_data, char* text, Vec2 position, Color color) { VertexBuffer const vertex_array = get_scratch_vertex_array(); const size_t len = SDL_strlen(text); VertexBufferBuilder builder = build_vertex_buffer(vertex_array, sizeof (ElementIndexedQuadWithoutColor) * len); for (size_t i = 0; i < len; ++i) { const char c = text[i]; /* outside the range of what we want to display */ //if (c < ASCII_START || c > ASCII_END) if (c < ASCII_START) continue; /* stb_truetype.h conveniently provides everything we need to draw here! */ stbtt_aligned_quad quad; stbtt_GetPackedQuad( font_data->char_data, (int)ctx.font_texture_size, (int)ctx.font_texture_size, c - ASCII_START, &position.x, &position.y, &quad, true ); /* have to do this so the "origin" is at the top left */ /* maybe there's a better way, or maybe this isn't a good idea... */ /* who knows */ quad.y0 += (float)font_data->ascent; quad.y1 += (float)font_data->ascent; ElementIndexedQuadWithoutColor const payload = { .v0 = (Vec2){ quad.x0, quad.y0 }, .v1 = (Vec2){ quad.x1, quad.y0 }, .v2 = (Vec2){ quad.x1, quad.y1 }, .v3 = (Vec2){ quad.x0, quad.y1 }, .uv0 = (Vec2){ quad.s0, quad.t0 }, .uv1 = (Vec2){ quad.s1, quad.t0 }, .uv2 = (Vec2){ quad.s1, quad.t1 }, .uv3 = (Vec2){ quad.s0, quad.t1 }, }; ((ElementIndexedQuadWithoutColor *)builder.base)[i] = payload; } finish_vertex_builder(&builder); finally_draw_text(font_data, len, color, vertex_array); } static bool ensure_font_cache(const char *font_path, int height_px) { /* HACK: don't */ bool is_cached = false; for (size_t i = 0; i < arrlenu(ctx.text_cache.data); ++i) { FontData *font_data = ctx.text_cache.data[i]; if ((SDL_strcmp(font_path, font_data->file_path) == 0) && height_px == font_data->height_px) { is_cached = true; break; } } if (!is_cached) { FontData *new_font_data = text_load_font_data(font_path, height_px); if (new_font_data == NULL) return false; arrput(ctx.text_cache.data, new_font_data); } return true; } static FontData *get_font_data(const char *font_path, int height_px) { FontData *font_data = NULL; for (size_t i = 0; i < arrlenu(ctx.text_cache.data); ++i) { font_data = ctx.text_cache.data[i]; if ((SDL_strcmp(font_path, font_data->file_path) == 0) && height_px == font_data->height_px) { break; } } return font_data; } void render_text(const TextPrimitive *text) { FontData *font_data = get_font_data(text->font, text->height_px); text_draw_with(font_data, text->text, text->position, text->color); } void text_cache_init(TextCache *cache) { arrsetlen(cache->data, 0); string_arena_init(&string_arena); sh_new_arena(font_file_cache_hash); } void text_cache_deinit(TextCache *cache) { for (size_t i = 0; i < arrlenu(ctx.text_cache.data); ++i) { text_destroy_font_data(ctx.text_cache.data[i]); } for (size_t i = 0; i < shlenu(font_file_cache_hash); ++i) { SDL_free(font_file_cache_hash[i].value.buffer); } shfree(font_file_cache_hash); string_arena_deinit(&string_arena); arrfree(cache->data); } void text_cache_reset_arena(TextCache *cache) { (void)cache; string_arena_reset(&string_arena); } void draw_text(const char *string, Vec2 position, float height, Color color, const char *font) { if (!font) { log_warn("Default font isn't yet implemented"); return; } if (!ensure_font_cache(font, (int)height)) return; /* the original string might not be around by the time it's used, so copy it */ size_t str_length = SDL_strlen(string) + 1; if (str_length == 1) /* empty string */ return; char *dup_string = string_arena_alloc(&string_arena, str_length); SDL_strlcpy(dup_string, string, str_length); TextPrimitive text = { .color = color, .position = position, .text = dup_string, .font = font, .height_px = (int)height, }; Primitive2D primitive = { .type = PRIMITIVE_2D_TEXT, .text = text, }; arrput(ctx.render_queue_2d, primitive); } float draw_text_width(const char *string, float height, const char *font) { if (!ensure_font_cache(font, (int)height)) return 0; FontData *font_data = get_font_data(font, (int)height); int length = 0; for (const char *p = string; *p != '\0'; ++p) { const char c = *p; int advance_width = 0; int left_side_bearing = 0; stbtt_GetCodepointHMetrics(&font_data->info, (int)c, &advance_width, &left_side_bearing); length += advance_width; } return (float)length * font_data->scale_factor; } void finally_draw_text(FontData const *font_data, size_t len, Color color, VertexBuffer buffer) { DeferredCommandDraw command = {0}; command.vertices = (AttributeArrayPointer) { .arity = 2, .type = TWN_FLOAT, .stride = offsetof(ElementIndexedQuadWithoutColor, v1), .offset = offsetof(ElementIndexedQuadWithoutColor, v0), .buffer = buffer }; command.texcoords = (AttributeArrayPointer) { .arity = 2, .type = TWN_FLOAT, .stride = offsetof(ElementIndexedQuadWithoutColor, v1), .offset = offsetof(ElementIndexedQuadWithoutColor, uv0), .buffer = buffer }; command.constant_colored = true; command.color = color; command.gpu_texture = font_data->texture; command.uses_gpu_key = true; command.textured = true; command.element_buffer = get_quad_element_buffer(); command.element_count = 6 * (uint32_t)len; command.range_end = 6 * (uint32_t)len; command.texture_mode = TEXTURE_MODE_GHOSTLY; command.pipeline = PIPELINE_2D; command.depth_range_high = depth_range_high; command.depth_range_low = depth_range_low; DeferredCommand final_command = { .type = DEFERRED_COMMAND_TYPE_DRAW, .draw = command }; arrpush(deferred_commands, final_command); /* TODO: why doesn't it get restored if not placed here? */ // glDepthMask(GL_TRUE); }