vtgl

terminal emulator implemented in OpenGL
git clone anongit@rnpnr.xyz:vtgl.git
Log | Files | Refs | Feed | LICENSE

Commit: a9781ad8f2a0d97c0685ffc9c871c2ee638f1e29
Parent: 3febfbd497ad1c5479edc7f8c618867aeb11c91d
Author: Randy Palamar
Date:   Sat, 19 Oct 2024 18:09:14 -0600

start packing glyphs into a 2D font atlas

this is going to be better for doing layout on the GPU. while we
are at it glyphs were aligned within cell boundaries in the
existing texture so that they don't need an offset anymore.

Diffstat:
Mfont.c | 71++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Mmain.c | 57+++++++++++++++++++++++++++++++++++----------------------
Mterminal.c | 2++
Mutil.h | 21++++++++++++++++-----
Mvtgl.c | 115++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
5 files changed, 165 insertions(+), 101 deletions(-)

diff --git a/font.c b/font.c @@ -127,7 +127,8 @@ render_glyph(Arena *a, FontAtlas *fa, u32 cp, enum face_style style, CachedGlyph /* NOTE: first check if glyph is in the cache and valid */ /* NOTE: 8 MSB are not used for UTF8 so we can use that to store the style of the glyph */ - u32 idx = get_glyph_entry_index(&fa->glyph_cache, cp|((u32)style << 24)); + u32 packed_cp = cp | ((u32)(style) << 24); + u32 idx = get_glyph_entry_index(&fa->glyph_cache, packed_cp); CachedGlyph *cg = fa->glyph_cache.glyphs + idx; *out_idx = idx; @@ -156,31 +157,42 @@ render_glyph(Arena *a, FontAtlas *fa, u32 cp, enum face_style style, CachedGlyph stbtt_GetGlyphBitmapBoxSubpixel(&f->font_info, glyph_idx, scale, scale, 0, 0, &x0, &y0, &x1, &y1); - cg->g.size.w = x1 - x0; - cg->g.size.h = y1 - y0; - cg->g.delta.x = x0; - cg->g.delta.y = -y1; - - u8 *render_buf = alloc(a, u8, cg->g.size.w * cg->g.size.h); - stbtt_MakeGlyphBitmapSubpixel(*a, &f->font_info, render_buf, cg->g.size.w, cg->g.size.h, - cg->g.size.w, scale, scale, 0, 0, glyph_idx); - - rgba_bitmap = alloc(a, u32, cg->g.size.w * cg->g.size.h); - for (u32 i = 0; i < cg->g.size.h; i++) { - for (u32 j = 0; j < cg->g.size.w; j++) { + i32 height = y1 - y0; + i32 width = x1 - x0; + u8 *render_buf = alloc(a, u8, width * height); + stbtt_MakeGlyphBitmapSubpixel(*a, &f->font_info, render_buf, width, height, + width, scale, scale, 0, 0, glyph_idx); + + /* NOTE: this looks wierd but some 'wide' glyphs are not actually wide but still + * need to be displayed as such (eg. ト). x1 can be used to determine this. */ + cg->tile_count = (u16)(width + (fa->info.w + 1) / 2) / fa->info.w; + if (cg->tile_count * fa->info.w < x1) cg->tile_count++; + ASSERT(cg->tile_count * fa->info.w >= x1); + + i32 out_width = fa->info.w * cg->tile_count; + i32 out_size = fa->info.h * out_width; + cg->size.w = out_width; + cg->size.h = fa->info.h; + + ASSERT(out_width >= width); + + rgba_bitmap = alloc(a, u32, out_size); + u32 out_y = (fa->info.h + y0 - fa->info.baseline) * out_width; + for (i32 i = 0; i < height; i++) { + u32 out_x = x0; + for (i32 j = 0; j < width; j++) { /* TODO: handled coloured glyphs */ u32 pixel = 0; if (0 /* COLOURED */) { } else { - pixel = render_buf[i * cg->g.size.w + j] << 24 | 0x00FFFFFF; + pixel = render_buf[i * width + j] << 24 | 0x00FFFFFF; } - rgba_bitmap[i * cg->g.size.w + j] = pixel; + rgba_bitmap[out_y + out_x] = pixel; + out_x++; } + out_y += out_width; } - /* NOTE: ' ' has 0 size in freetype but we need it to have a width! */ - if (cp == ' ') cg->g.size.w = fa->info.w; - end: END_TIMED_BLOCK(); @@ -199,15 +211,28 @@ update_font_metrics(Font *font, FontInfo *info) } static void -font_atlas_update(FontAtlas *fa) +font_atlas_update(FontAtlas *fa, iv2 glyph_bitmap_dim) { GlyphCache *gc = &fa->glyph_cache; mem_clear(gc->glyphs, 0, sizeof(*gc->glyphs) * gc->cache_len); mem_clear(gc->hash_table, 0, sizeof(*gc->hash_table) * gc->cache_len); - for(u32 i = 0; i < gc->cache_len - 1; i++) - gc->glyphs[i].next_with_same_hash = i + 1; get_and_clear_glyph_cache_stats(gc); update_font_metrics(&fa->fonts[0][FS_NORMAL], &fa->info); + + gc->cursor = (iv2){0}; + gc->tiles_in_x = glyph_bitmap_dim.x / fa->info.w; + ASSERT(gc->tiles_in_x); + + u32 x = 0, y = 0; + for (u32 i = 0; i < gc->cache_len; i++, x++) { + if (i + 1 < gc->cache_len) + gc->glyphs[i].next_with_same_hash = i + 1; + if (x >= gc->tiles_in_x) { + x = 0; + y++; + } + gc->glyphs[i].gpu_glyph_index = (y << 16) | x; + } } static void @@ -227,7 +252,7 @@ shift_font_sizes(FontAtlas *fa, i32 size_delta) } static void -init_fonts(Term *t, Arena *a) +init_fonts(Term *t, Arena *a, iv2 glyph_bitmap_dim) { FontAtlas *fa = &t->fa; u32 n_fonts = ARRAY_COUNT(g_fonts); @@ -247,5 +272,5 @@ init_fonts(Term *t, Arena *a) gc->glyphs = alloc(a, typeof(*gc->glyphs), gc->cache_len); gc->hash_table = alloc(a, typeof(*gc->hash_table), gc->cache_len); - font_atlas_update(fa); + font_atlas_update(&t->fa, glyph_bitmap_dim); } diff --git a/main.c b/main.c @@ -94,28 +94,9 @@ error_callback(int code, const char *desc) } static void -init_window(Term *t, Arena arena, iv2 requested_size) +init_window(Term *t, Arena arena, iv2 window_size) { - if (!glfwInit()) - os_die("Failed to init GLFW\n"); - glfwSetErrorCallback(error_callback); - - GLFWmonitor *mon = glfwGetPrimaryMonitor(); - if (!mon) { - glfwTerminate(); - os_die("Failed to get GLFW monitor\n"); - } - - iv2 ws; - glfwGetMonitorWorkarea(mon, NULL, NULL, &ws.w, &ws.h); - - if (requested_size.w > 0 && requested_size.h > 0) { - t->gl.window_size = (v2){.w = requested_size.w, .h = requested_size.h}; - glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE); - glfwWindowHint(GLFW_FLOATING, GLFW_TRUE); - } else { - t->gl.window_size = (v2){.w = ws.w, .h = ws.h}; - } + t->gl.window_size = (v2){.w = window_size.w, .h = window_size.h}; glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); @@ -169,6 +150,17 @@ init_window(Term *t, Arena arena, iv2 requested_size) glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA8, + MAX_FONT_SIZE, MAX_FONT_SIZE, TEXTURE_GLYPH_COUNT, + 0, GL_RED, GL_UNSIGNED_BYTE, 0); + + glGenTextures(1, &t->gl.glyph_bitmap_tex); + glBindTexture(GL_TEXTURE_2D, t->gl.glyph_bitmap_tex); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, + t->gl.glyph_bitmap_dim.w, t->gl.glyph_bitmap_dim.h, + 0, GL_RGBA, GL_UNSIGNED_BYTE, 0); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); @@ -337,8 +329,29 @@ main(i32 argc, char *argv[]) } do_debug(NULL); + + if (!glfwInit()) + os_die("Failed to init GLFW\n"); + glfwSetErrorCallback(error_callback); + + GLFWmonitor *mon = glfwGetPrimaryMonitor(); + if (!mon) { + glfwTerminate(); + os_die("Failed to get GLFW monitor\n"); + } + + iv2 ws; + glfwGetMonitorWorkarea(mon, NULL, NULL, &ws.w, &ws.h); + term.gl.glyph_bitmap_dim = ws; + iv2 requested_size = init_term(&term, &memory, cells); - init_window(&term, memory, requested_size); + if (requested_size.w > 0 && requested_size.h > 0) { + glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE); + glfwWindowHint(GLFW_FLOATING, GLFW_TRUE); + ws = requested_size; + } + + init_window(&term, memory, ws); init_callbacks(&term.gl); os_alloc_ring_buffer(&term.views[0].log, BACKLOG_SIZE); diff --git a/terminal.c b/terminal.c @@ -1192,6 +1192,8 @@ push_line(Term *t, Line *line, Arena a) i32 width; if (line->has_unicode) { + /* TODO: this needs to be determined from the CachedGlyph tile count + * wcwidth does not report the correct result for all characters */ width = wcwidth(cp); ASSERT(width != -1); } else { diff --git a/util.h b/util.h @@ -26,6 +26,8 @@ #define MIN(a, b) ((a) <= (b) ? (a) : (b)) #define SGN(a) ((a) < 0 ? (-1) : (1)) +#define SAFE_RATIO_1(a, b) ((b) ? (a) / (b) : (a)) + #define ISCONTROLC0(c) (BETWEEN((c), 0, 0x1F) || (c) == 0x7F) #define ISCONTROL(c) (ISCONTROLC0(c)) #define ISSPACE(c) ((c) == ' ' || (c) == '\n' || (c) == '\t') @@ -261,6 +263,9 @@ typedef struct { u32 render_shader_ubo; u32 render_shader_ssbo; + iv2 glyph_bitmap_dim; + u32 glyph_bitmap_tex; + u32 flags; u32 mode; @@ -281,6 +286,7 @@ typedef struct { } RenderPushBuffer; #define MAX_FONT_SIZE 128 +#define TEXTURE_GLYPH_COUNT PUSH_BUFFER_CAP enum conversion_status { CR_FAILURE, CR_SUCCESS }; struct conversion_result { @@ -341,11 +347,13 @@ typedef struct { } Glyph; typedef struct { - Glyph g; - u32 cp; - u32 next_with_same_hash; - u16 prev, next; - b32 uploaded_to_gpu; + uv2 size; + u32 cp; + u32 next_with_same_hash; + u16 prev, next; + u32 gpu_glyph_index; + b32 uploaded_to_gpu; + u16 tile_count; } CachedGlyph; typedef union { @@ -362,6 +370,9 @@ typedef struct { u32 *hash_table; u32 cache_len; GlyphCacheStats stats; + u32 tiles_in_x; + /* TODO: temporary. this is need this to be able to keep track the end of wide glyphs */ + iv2 cursor; } GlyphCache; typedef struct { diff --git a/vtgl.c b/vtgl.c @@ -17,8 +17,6 @@ static void draw_debug_overlay(Term *, RenderPushBuffer *); #define REVERSE_VIDEO_MASK (Colour){.r = 0xff, .g = 0xff, .b = 0xff}.rgba -#define TEXTURE_GLYPH_COUNT PUSH_BUFFER_CAP - static v4 normalized_colour(Colour c) { @@ -55,16 +53,16 @@ set_projection_matrix(GLCtx *gl) } static v2 -get_cell_size(Term *t) +get_cell_size(FontAtlas *fa) { - v2 result = {.w = t->fa.info.w, .h = t->fa.info.h}; + v2 result = {.w = fa->info.w, .h = fa->info.h}; return result; } static v2 get_occupied_size(Term *t) { - v2 cs = get_cell_size(t); + v2 cs = get_cell_size(&t->fa); v2 result = {.x = t->size.w * cs.w, .y = t->size.h * cs.h}; return result; } @@ -101,7 +99,7 @@ resize(Term *t) ws.h -= 2 * g_term_margin.h; uv2 old_size = t->size; - v2 cs = get_cell_size(t); + v2 cs = get_cell_size(&t->fa); t->size.w = (u32)(ws.w / cs.w); t->size.h = (u32)(ws.h / cs.h); @@ -121,18 +119,6 @@ resize(Term *t) } static void -update_font_textures(GLCtx *gl, FontAtlas *fa) -{ - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D_ARRAY, gl->glyph_tex); - glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA8, - MAX_FONT_SIZE, MAX_FONT_SIZE, TEXTURE_GLYPH_COUNT, - 0, GL_RED, GL_UNSIGNED_BYTE, 0); - - font_atlas_update(fa); -} - -static void update_uniforms(Term *t, enum shader_stages stage) { switch (stage) { @@ -154,23 +140,55 @@ update_uniforms(Term *t, enum shader_stages stage) } set_projection_matrix(&t->gl); - /* TODO: this doesn't need to be called so often */ - update_font_textures(&t->gl, &t->fa); +} + +static uv2 +unpack_gpu_glyph_index(u32 gpu_glyph_index) +{ + uv2 result = {.x = gpu_glyph_index & 0xFFFF, .y = gpu_glyph_index >> 16}; + return result; +} + +static iv2 +get_gpu_texture_position(v2 cs, uv2 gpu_position) +{ + iv2 result = {.y = gpu_position.y * cs.y, .x = gpu_position.x * cs.x}; + return result; } static i32 -get_gpu_glyph_index(Arena a, FontAtlas *fa, u32 codepoint, enum face_style style, Glyph *out_glyph) +get_gpu_glyph_index(Arena a, GLCtx *gl, FontAtlas *fa, u32 codepoint, enum face_style style, uv2 *glyph_size) { u32 depth_idx; CachedGlyph *cg; u32 *data = render_glyph(&a, fa, codepoint, style, &cg, &depth_idx); - *out_glyph = cg->g; + *glyph_size = cg->size; if (data) { ASSERT(depth_idx); + glBindTexture(GL_TEXTURE_2D_ARRAY, gl->glyph_tex); glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, depth_idx, - out_glyph->size.w, out_glyph->size.h, 1, GL_RGBA, + glyph_size->w, glyph_size->h, 1, GL_RGBA, GL_UNSIGNED_BYTE, data); cg->uploaded_to_gpu = 1; + + /* TODO: this will be bound beforehand */ + glBindTexture(GL_TEXTURE_2D, gl->glyph_bitmap_tex); + + /* TODO: the glyph cache needs to manage the positioning in the texture */ + GlyphCache *gc = &fa->glyph_cache; + uv2 gpu_position = unpack_gpu_glyph_index(cg->gpu_glyph_index); + if (gpu_position.x + cg->tile_count - 1 >= gc->tiles_in_x) { + gpu_position.x = 0; + gpu_position.y++; + } + iv2 glyph_position = get_gpu_texture_position(get_cell_size(fa), gpu_position); + ASSERT(glyph_position.x < gl->glyph_bitmap_dim.x && + glyph_position.y < gl->glyph_bitmap_dim.y); + + glTexSubImage2D(GL_TEXTURE_2D, 0, glyph_position.x, glyph_position.y, + cg->size.w, cg->size.h, GL_RGBA, GL_UNSIGNED_BYTE, data); + + glBindTexture(GL_TEXTURE_2D_ARRAY, gl->glyph_tex); } ASSERT(cg->uploaded_to_gpu); return depth_idx; @@ -218,7 +236,7 @@ push_empty_cell_rect(RenderPushBuffer *rpb, Term *t, u32 minrow, u32 maxrow, u32 ASSERT(minrow <= maxrow && mincol <= maxcol); ASSERT(maxrow <= t->size.h && maxcol <= t->size.w); - v2 cs = get_cell_size(t); + v2 cs = get_cell_size(&t->fa); v2 size = {.x = (maxcol - mincol + 1) * cs.w, .y = (maxrow - minrow + 1) * cs.h}; v2 pos = {.x = mincol * cs.w, .y = t->gl.window_size.h - cs.h * (maxrow + 1)}; @@ -233,28 +251,25 @@ draw_rectangle(RenderPushBuffer *rpb, GLCtx *gl, Rect r, Colour colour) } static void -push_cell(RenderPushBuffer *rpb, GLCtx *gl, Arena a, FontAtlas *fa, Cell c, Rect r, f32 font_y_delta) +push_cell(RenderPushBuffer *rpb, GLCtx *gl, FontAtlas *fa, Arena a, Cell c, Rect r) { BEGIN_TIMED_BLOCK(); u32 idx = get_render_push_buffer_idx(rpb, gl, 2); ASSERT(c.cp); - Glyph g; - i32 depth_idx = get_gpu_glyph_index(a, fa, c.cp, c.style.attr & FS_MASK, &g); + uv2 g_size; + i32 depth_idx = get_gpu_glyph_index(a, gl, fa, c.cp, c.style.attr & FS_MASK, &g_size); rpb->vertscales[idx + 0] = r.size; - rpb->vertscales[idx + 1] = (v2){.x = g.size.w, .y = g.size.h}; + rpb->vertscales[idx + 1] = (v2){.x = g_size.w, .y = g_size.h}; rpb->vertoffsets[idx + 0] = r.pos; - rpb->vertoffsets[idx + 1] = (v2){ - .x = r.pos.x + g.delta.x, - .y = r.pos.y + g.delta.y + font_y_delta, - }; + rpb->vertoffsets[idx + 1] = r.pos; rpb->texscales[idx + 0] = (v2){0}; rpb->texscales[idx + 1] = (v2){ - .x = g.size.w / (f32)MAX_FONT_SIZE, - .y = g.size.h / (f32)MAX_FONT_SIZE, + .x = g_size.w / (f32)MAX_FONT_SIZE, + .y = g_size.h / (f32)MAX_FONT_SIZE, }; rpb->charmap[idx + 0] = depth_idx; @@ -275,8 +290,8 @@ push_cell(RenderPushBuffer *rpb, GLCtx *gl, Arena a, FontAtlas *fa, Cell c, Rect } static void -push_cell_row(RenderPushBuffer *rpb, GLCtx *gl, Arena a, FontAtlas *fa, Cell *row, u32 len, - b32 inverse, Rect cr, f32 font_y_delta) +push_cell_row(RenderPushBuffer *rpb, GLCtx *gl, FontAtlas *fa, Arena a, Cell *row, u32 len, + b32 inverse, Rect cr) { ASSERT(inverse == 0 || inverse == 1); v2 cs = cr.size; @@ -288,7 +303,7 @@ push_cell_row(RenderPushBuffer *rpb, GLCtx *gl, Arena a, FontAtlas *fa, Cell *ro if (cell.style.attr & ATTR_WIDE) cr.size = csw; else cr.size = cs; cell.style.attr ^= inverse * ATTR_INVERSE; - push_cell(rpb, gl, a, fa, cell, cr, font_y_delta); + push_cell(rpb, gl, fa, a, cell, cr); cr.pos.x += cr.size.w; } } @@ -303,16 +318,13 @@ render_framebuffer(Term *t, RenderPushBuffer *rpb) BEGIN_TIMED_BLOCK(); v2 tl = get_terminal_top_left(t); - v2 cs = get_cell_size(t); + v2 cs = get_cell_size(&t->fa); Rect cr = {.pos = {.x = tl.x, .y = tl.y - cs.h}, .size = cs}; - f32 font_y_delta = t->fa.info.baseline; - TermView *tv = t->views + t->view_idx; /* NOTE: draw whole framebuffer */ for (u32 r = 0; r < t->size.h; r++) { - push_cell_row(rpb, &t->gl, t->arena_for_frame, &t->fa, tv->fb.rows[r], t->size.w, 0, - cr, font_y_delta); + push_cell_row(rpb, &t->gl, &t->fa, t->arena_for_frame, tv->fb.rows[r], t->size.w, 0, cr); cr.pos.y -= cs.h; } @@ -326,15 +338,15 @@ render_framebuffer(Term *t, RenderPushBuffer *rpb) /* NOTE: do full rows first */ for (; curs.y < end.y; curs.y++) { u32 len = t->size.w - curs.x - 1; - push_cell_row(rpb, &t->gl, t->arena_for_frame, &t->fa, - tv->fb.rows[curs.y] + curs.x, len, 1, cr, font_y_delta); + push_cell_row(rpb, &t->gl, &t->fa, t->arena_for_frame, + tv->fb.rows[curs.y] + curs.x, len, 1, cr); curs.x = 0; cr.pos.x = tl.x; cr.pos.y -= cs.h; } /* NOTE: do the last row */ - push_cell_row(rpb, &t->gl, t->arena_for_frame, &t->fa, tv->fb.rows[curs.y] + curs.x, - end.x - curs.x + 1, 1, cr, font_y_delta); + push_cell_row(rpb, &t->gl, &t->fa, t->arena_for_frame, tv->fb.rows[curs.y] + curs.x, + end.x - curs.x + 1, 1, cr); } /* NOTE: draw cursor */ @@ -349,7 +361,7 @@ render_framebuffer(Term *t, RenderPushBuffer *rpb) cursor.style.attr ^= ATTR_INVERSE; if ((t->mode & TM_ALTSCREEN) == 0) cursor.style.attr |= ATTR_BLINK; - push_cell(rpb, &t->gl, t->arena_for_frame, &t->fa, cursor, cr, font_y_delta); + push_cell(rpb, &t->gl, &t->fa, t->arena_for_frame, cursor, cr); } END_TIMED_BLOCK(); @@ -359,7 +371,7 @@ static iv2 mouse_to_cell_space(Term *t, v2 mouse) { iv2 result = {0}; - v2 cell_size = get_cell_size(t); + v2 cell_size = get_cell_size(&t->fa); v2 bot_left = get_terminal_bot_left(t); result.x = (i32)((mouse.x - bot_left.x) / cell_size.w); @@ -515,7 +527,7 @@ KEYBIND_FN(scroll) KEYBIND_FN(zoom) { shift_font_sizes(&t->fa, a.i); - update_font_textures(&t->gl, &t->fa); + font_atlas_update(&t->fa, t->gl.glyph_bitmap_dim); t->gl.flags |= NEEDS_RESIZE; return 1; } @@ -725,18 +737,19 @@ scroll_callback(GLFWwindow *win, f64 xoff, f64 yoff) DEBUG_EXPORT iv2 init_term(Term *t, Arena *a, iv2 cells) { - init_fonts(t, a); + init_fonts(t, a, t->gl.glyph_bitmap_dim); for (u32 i = 0; i < ARRAY_COUNT(t->saved_cursors); i++) { cursor_reset(t); cursor_move_to(t, 0, 0); cursor_alt(t, 1); } selection_clear(&t->selection); - v2 cs = get_cell_size(t); + v2 cs = get_cell_size(&t->fa); iv2 requested_size = { .x = cs.x * cells.x + 2 * g_term_margin.x, .y = cs.y * cells.y + 2 * g_term_margin.y, }; + return requested_size; }