2024-10-07 14:53:09 +00:00
|
|
|
#include "twn_draw_c.h"
|
|
|
|
#include "twn_draw.h"
|
2024-09-16 13:17:00 +00:00
|
|
|
#include "twn_util.h"
|
2024-10-12 18:16:25 +00:00
|
|
|
#include "twn_util_c.h"
|
2024-09-16 13:17:00 +00:00
|
|
|
#include "twn_engine_context_c.h"
|
2024-08-23 02:41:52 +00:00
|
|
|
|
|
|
|
#include <stb_truetype.h>
|
2024-10-13 15:14:36 +00:00
|
|
|
#include <stb_ds.h>
|
2024-08-23 02:41:52 +00:00
|
|
|
|
|
|
|
#define ASCII_START 32
|
|
|
|
#define ASCII_END 128
|
|
|
|
#define NUM_DISPLAY_ASCII ((ASCII_END - ASCII_START) + 1)
|
|
|
|
|
2024-09-27 18:02:24 +00:00
|
|
|
/* 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;
|
|
|
|
|
|
|
|
|
2024-10-14 00:34:05 +00:00
|
|
|
typedef struct FontFileBuffer {
|
|
|
|
size_t len;
|
|
|
|
unsigned char *buffer;
|
|
|
|
} FontFileBuffer;
|
|
|
|
|
|
|
|
typedef struct FontFileCacheItem {
|
|
|
|
char *key;
|
|
|
|
FontFileBuffer value;
|
|
|
|
} FontFileCacheItem;
|
|
|
|
|
|
|
|
static FontFileCacheItem *font_file_cache_hash;
|
|
|
|
|
|
|
|
|
2024-09-27 18:02:24 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2024-08-23 02:41:52 +00:00
|
|
|
|
2024-09-23 17:43:16 +00:00
|
|
|
static FontData *text_load_font_data(const char *path, int height_px) {
|
|
|
|
FontData *font_data = ccalloc(1, sizeof *font_data);
|
2024-08-23 02:41:52 +00:00
|
|
|
font_data->file_path = path;
|
|
|
|
font_data->height_px = height_px;
|
|
|
|
|
2024-10-01 00:13:58 +00:00
|
|
|
unsigned char* bitmap = ccalloc(ctx.font_texture_size * ctx.font_texture_size, 1);
|
2024-08-23 02:41:52 +00:00
|
|
|
|
|
|
|
{
|
2024-10-14 00:34:05 +00:00
|
|
|
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);
|
|
|
|
FontFileBuffer buffer = { buf_len, buf };
|
|
|
|
shput(font_file_cache_hash, path, buffer);
|
|
|
|
}
|
|
|
|
|
2024-08-23 02:41:52 +00:00
|
|
|
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;
|
2024-10-01 00:13:58 +00:00
|
|
|
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);
|
2024-08-23 02:41:52 +00:00
|
|
|
stbtt_PackFontRange(&pctx, buf, 0, (float)height_px, ASCII_START, NUM_DISPLAY_ASCII, font_data->char_data);
|
|
|
|
stbtt_PackEnd(&pctx);
|
|
|
|
}
|
|
|
|
|
2024-10-01 00:13:58 +00:00
|
|
|
font_data->texture = create_gpu_texture(ctx.font_filtering, true);
|
2024-09-16 13:17:00 +00:00
|
|
|
upload_gpu_texture(
|
2024-09-16 06:07:01 +00:00
|
|
|
font_data->texture,
|
|
|
|
bitmap,
|
|
|
|
1,
|
2024-10-01 00:13:58 +00:00
|
|
|
(int)ctx.font_texture_size,
|
|
|
|
(int)ctx.font_texture_size
|
2024-08-23 02:41:52 +00:00
|
|
|
);
|
2024-09-25 22:42:34 +00:00
|
|
|
SDL_free(bitmap);
|
2024-08-23 02:41:52 +00:00
|
|
|
|
|
|
|
return font_data;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2024-09-23 17:43:16 +00:00
|
|
|
static void text_destroy_font_data(FontData *font_data) {
|
2024-09-16 06:07:01 +00:00
|
|
|
delete_gpu_texture(font_data->texture);
|
2024-09-25 22:42:34 +00:00
|
|
|
SDL_free(font_data);
|
2024-08-23 02:41:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2024-09-23 17:43:16 +00:00
|
|
|
static void text_draw_with(FontData* font_data, char* text, Vec2 position, Color color) {
|
2024-10-15 12:29:45 +00:00
|
|
|
VertexBuffer const vertex_array = get_scratch_vertex_array();
|
2024-08-23 02:41:52 +00:00
|
|
|
|
2024-09-16 06:07:01 +00:00
|
|
|
const size_t len = SDL_strlen(text);
|
2024-08-23 02:41:52 +00:00
|
|
|
|
2025-01-17 19:48:35 +00:00
|
|
|
VertexBufferBuilder builder = build_vertex_buffer(vertex_array, sizeof (ElementIndexedQuadWithoutColor) * len);
|
2024-08-23 02:41:52 +00:00
|
|
|
|
2024-09-16 06:07:01 +00:00
|
|
|
for (size_t i = 0; i < len; ++i) {
|
|
|
|
const char c = text[i];
|
2024-08-23 02:41:52 +00:00
|
|
|
|
|
|
|
/* 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,
|
2024-10-01 00:13:58 +00:00
|
|
|
(int)ctx.font_texture_size,
|
|
|
|
(int)ctx.font_texture_size,
|
2024-08-23 02:41:52 +00:00
|
|
|
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;
|
|
|
|
|
2025-01-17 19:48:35 +00:00
|
|
|
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;
|
2024-08-23 02:41:52 +00:00
|
|
|
}
|
|
|
|
|
2025-01-17 19:48:35 +00:00
|
|
|
finish_vertex_builder(&builder);
|
2024-09-16 06:07:01 +00:00
|
|
|
finally_draw_text(font_data, len, color, vertex_array);
|
2024-08-23 02:41:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static void ensure_font_cache(const char *font_path, int height_px) {
|
2024-10-14 00:34:05 +00:00
|
|
|
/* HACK: don't */
|
2024-08-23 02:41:52 +00:00
|
|
|
bool is_cached = false;
|
|
|
|
for (size_t i = 0; i < arrlenu(ctx.text_cache.data); ++i) {
|
2024-09-23 17:43:16 +00:00
|
|
|
FontData *font_data = ctx.text_cache.data[i];
|
2024-09-25 22:42:34 +00:00
|
|
|
if ((SDL_strcmp(font_path, font_data->file_path) == 0) && height_px == font_data->height_px) {
|
2024-08-23 02:41:52 +00:00
|
|
|
is_cached = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!is_cached) {
|
2024-09-23 17:43:16 +00:00
|
|
|
FontData *new_font_data = text_load_font_data(font_path, height_px);
|
2024-08-23 02:41:52 +00:00
|
|
|
arrput(ctx.text_cache.data, new_font_data);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2024-09-23 17:43:16 +00:00
|
|
|
static FontData *get_font_data(const char *font_path, int height_px) {
|
|
|
|
FontData *font_data = NULL;
|
2024-08-23 02:41:52 +00:00
|
|
|
for (size_t i = 0; i < arrlenu(ctx.text_cache.data); ++i) {
|
|
|
|
font_data = ctx.text_cache.data[i];
|
2024-09-25 22:42:34 +00:00
|
|
|
if ((SDL_strcmp(font_path, font_data->file_path) == 0) && height_px == font_data->height_px) {
|
2024-08-23 02:41:52 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return font_data;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2024-09-23 17:43:16 +00:00
|
|
|
void render_text(const TextPrimitive *text) {
|
|
|
|
FontData *font_data = get_font_data(text->font, text->height_px);
|
2024-08-23 02:41:52 +00:00
|
|
|
text_draw_with(font_data, text->text, text->position, text->color);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2024-09-23 17:43:16 +00:00
|
|
|
void text_cache_init(TextCache *cache) {
|
2024-08-23 02:41:52 +00:00
|
|
|
arrsetlen(cache->data, 0);
|
2024-09-27 18:02:24 +00:00
|
|
|
string_arena_init(&string_arena);
|
2024-10-14 00:34:05 +00:00
|
|
|
|
|
|
|
sh_new_arena(font_file_cache_hash);
|
2024-08-23 02:41:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2024-09-23 17:43:16 +00:00
|
|
|
void text_cache_deinit(TextCache *cache) {
|
2024-08-23 02:41:52 +00:00
|
|
|
for (size_t i = 0; i < arrlenu(ctx.text_cache.data); ++i) {
|
|
|
|
text_destroy_font_data(ctx.text_cache.data[i]);
|
|
|
|
}
|
|
|
|
|
2024-10-14 00:34:05 +00:00
|
|
|
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);
|
|
|
|
|
2024-09-27 18:02:24 +00:00
|
|
|
string_arena_deinit(&string_arena);
|
2024-10-14 00:34:05 +00:00
|
|
|
arrfree(cache->data);
|
2024-09-27 18:02:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void text_cache_reset_arena(TextCache *cache) {
|
|
|
|
(void)cache;
|
|
|
|
string_arena_reset(&string_arena);
|
2024-08-23 02:41:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2024-10-29 09:25:24 +00:00
|
|
|
void draw_text(const char *string, Vec2 position, float height, Color color, const char *font) {
|
|
|
|
ensure_font_cache(font, (int)height);
|
2024-08-23 02:41:52 +00:00
|
|
|
|
2024-09-25 22:42:34 +00:00
|
|
|
/* the original string might not be around by the time it's used, so copy it */
|
2024-09-27 18:02:24 +00:00
|
|
|
size_t str_length = SDL_strlen(string) + 1;
|
2024-10-07 20:40:57 +00:00
|
|
|
if (str_length == 1)
|
|
|
|
/* empty string */
|
|
|
|
return;
|
2024-09-27 18:02:24 +00:00
|
|
|
char *dup_string = string_arena_alloc(&string_arena, str_length);
|
|
|
|
SDL_strlcpy(dup_string, string, str_length);
|
2024-08-23 02:41:52 +00:00
|
|
|
|
2024-09-23 17:43:16 +00:00
|
|
|
TextPrimitive text = {
|
2024-08-23 02:41:52 +00:00
|
|
|
.color = color,
|
|
|
|
.position = position,
|
|
|
|
.text = dup_string,
|
2024-10-29 09:25:24 +00:00
|
|
|
.font = font,
|
|
|
|
.height_px = (int)height,
|
2024-08-23 02:41:52 +00:00
|
|
|
};
|
|
|
|
|
2024-09-23 17:43:16 +00:00
|
|
|
Primitive2D primitive = {
|
2024-08-23 02:41:52 +00:00
|
|
|
.type = PRIMITIVE_2D_TEXT,
|
|
|
|
.text = text,
|
|
|
|
};
|
|
|
|
|
|
|
|
arrput(ctx.render_queue_2d, primitive);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2024-10-29 09:25:24 +00:00
|
|
|
float draw_text_width(const char *string, float height, const char *font) {
|
|
|
|
ensure_font_cache(font, (int)height);
|
|
|
|
FontData *font_data = get_font_data(font, (int)height);
|
2024-08-23 02:41:52 +00:00
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2024-10-29 09:25:24 +00:00
|
|
|
return (float)length * font_data->scale_factor;
|
2024-08-23 02:41:52 +00:00
|
|
|
}
|
2025-01-05 16:46:05 +00:00
|
|
|
|
|
|
|
|
|
|
|
void finally_draw_text(FontData const *font_data,
|
|
|
|
size_t len,
|
|
|
|
Color color,
|
|
|
|
VertexBuffer buffer)
|
|
|
|
{
|
|
|
|
DeferredCommandDraw command = {0};
|
|
|
|
|
|
|
|
command.vertices = (AttributeArrayPointer) {
|
|
|
|
.arity = 2,
|
2025-01-14 20:20:54 +00:00
|
|
|
.type = TWN_FLOAT,
|
2025-01-05 16:46:05 +00:00
|
|
|
.stride = offsetof(ElementIndexedQuadWithoutColor, v1),
|
|
|
|
.offset = offsetof(ElementIndexedQuadWithoutColor, v0),
|
|
|
|
.buffer = buffer
|
|
|
|
};
|
|
|
|
|
|
|
|
command.texcoords = (AttributeArrayPointer) {
|
|
|
|
.arity = 2,
|
2025-01-14 20:20:54 +00:00
|
|
|
.type = TWN_FLOAT,
|
2025-01-05 16:46:05 +00:00
|
|
|
.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();
|
2025-01-14 20:20:54 +00:00
|
|
|
command.element_count = 6 * (uint32_t)len;
|
|
|
|
command.range_end = 6 * (uint32_t)len;
|
2025-01-05 16:46:05 +00:00
|
|
|
|
2025-01-14 20:20:54 +00:00
|
|
|
command.texture_mode = TEXTURE_MODE_GHOSTLY;
|
|
|
|
command.pipeline = PIPELINE_2D;
|
|
|
|
|
|
|
|
command.depth_range_high = depth_range_high;
|
|
|
|
command.depth_range_low = depth_range_low;
|
2025-01-05 16:46:05 +00:00
|
|
|
|
|
|
|
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);
|
|
|
|
}
|