vtgl

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

Commit: 9aa421f633daa234458ad5f905bc89927f49edda
Parent: a21c5f2b54962555719b33ec62cbee1b5c486ef8
Author: Randy Palamar
Date:   Sat, 22 Jun 2024 17:36:32 -0600

add ascii text rendering

Diffstat:
Afont.c | 89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mfrag.glsl | 14+++++++-------
Mmain.c | 6++++--
Mos_unix.c | 34++++++++++++++++++++++++++++++++++
Mutil.c | 2+-
Mutil.h | 55++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mvtgl.c | 96+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
7 files changed, 271 insertions(+), 25 deletions(-)

diff --git a/font.c b/font.c @@ -0,0 +1,89 @@ +/* See LICENSE for copyright details */ +static void +init_font(FT_Library *ftlib, Font *f, char *path, u32 fontsize) +{ + if (!*ftlib && FT_Init_FreeType(ftlib)) + die("Failed to init FreeType\n"); + + os_mapped_file map = os_map_file(path, OS_MAP_READ, OS_MAP_PRIVATE); + + f->bufsize = map.len; + f->buf = map.data; + + if (FT_New_Memory_Face(*ftlib, f->buf, f->bufsize, 0, &f->face)) + goto err1; + if (FT_Set_Pixel_Sizes(f->face, 0, fontsize)) + goto err2; + if (FT_Select_Charmap(f->face, FT_ENCODING_UNICODE)) + goto err2; + + return; +err2: + FT_Done_Face(f->face); +err1: + os_unmap_file(f->buf, f->bufsize); + die("Failed to init font: %s\n", path); +} + +static u8 * +render(FontAtlas *fa, Glyph *g, u32 cp) +{ + FT_GlyphSlot gs = NULL; + for (u32 i = 0; i < fa->nfonts; i++) { + FT_Face face = fa->fonts[i].face; + u32 fcp = FT_Get_Char_Index(face, cp); + if (fcp && !FT_Load_Glyph(face, fcp, FT_LOAD_RENDER)) { + gs = face->glyph; + break; + } + } + if (!gs) { + if (FT_Load_Glyph(fa->fonts[0].face, 0, FT_LOAD_RENDER)) + die("can't load replacement glyph\n"); + gs = fa->fonts[0].face->glyph; + } + + g->size.w = gs->bitmap.width; + g->size.h = gs->bitmap.rows; + g->delta.x = gs->bitmap_left; + g->delta.y = gs->bitmap_top - g->size.h; + + return gs->bitmap.buffer; +} + +static void +update_font_metrics(Term *t) +{ + t->fa.size.h = 0; + t->fa.size.w = 0; + t->fa.deltay = 0; + for (u32 i = ' '; i <= '~'; i++) { + Glyph g; + render(&t->fa, &g, i); + t->fa.size.h = MAX(t->fa.size.h, g.size.h); + t->fa.size.w = MAX(t->fa.size.w, g.size.w); + t->fa.deltay = MAX(t->fa.deltay, -g.delta.y); + t->glyph_cache[i - ' '] = g; + } +} + +static i32 +set_font_sizes(FontAtlas *fa, u32 fontsize) +{ + /* invalidate cache first incase a font fails to resize */ + for (u32 i = 0; i < fa->nfonts; i++) + if (FT_Set_Pixel_Sizes(fa->fonts[i].face, 0, fontsize)) + return -1; + return 0; +} + +static void +init_fonts(Term *t, char **paths, u32 npaths, u32 fontsize, Arena *a) +{ + t->fa.fonts = alloc(a, Font, npaths); + for (t->fa.nfonts = 0; t->fa.nfonts < npaths; t->fa.nfonts++) + init_font(&t->ftlib, t->fa.fonts + t->fa.nfonts, paths[t->fa.nfonts], fontsize); + + t->glyph_cache_len = 0x7E - 0x20 + 1; + t->glyph_cache = alloc(a, Glyph, t->glyph_cache_len); +} diff --git a/frag.glsl b/frag.glsl @@ -7,9 +7,9 @@ in VS_OUT { flat int index; } fs_in; -//uniform sampler2DArray u_texslot; -//uniform int u_charmap[512]; -//uniform vec2 u_texscale[512]; +uniform sampler2DArray u_texslot; +uniform int u_charmap[512]; +uniform vec2 u_texscale[512]; uniform uvec2 u_texcolour[512]; void main() @@ -17,9 +17,9 @@ void main() vec4 fg = unpackUnorm4x8(u_texcolour[fs_in.index].x).wzyx; vec4 bg = unpackUnorm4x8(u_texcolour[fs_in.index].y).wzyx; - //vec3 tex_coord = vec3(fs_in.tex_coord.xy, u_charmap[fs_in.index]); - //tex_coord.xy *= u_texscale[fs_in.index]; + vec3 tex_coord = vec3(fs_in.tex_coord.xy, u_charmap[fs_in.index]); + tex_coord.xy *= u_texscale[fs_in.index]; - //float a = u_texscale[fs_in.index].x == 0 ? 0 : texture(u_texslot, tex_coord).r; - colour = mix(bg, fg, fs_in.tex_coord.x); + float a = u_texscale[fs_in.index].x == 0 ? 0 : texture(u_texslot, tex_coord).r; + colour = mix(bg, fg, a); } diff --git a/main.c b/main.c @@ -6,8 +6,6 @@ #include "util.h" -#include "os_unix.c" - #ifndef _DEBUG static void do_debug(void) { } #else @@ -236,7 +234,11 @@ main(void) Arena memory = os_new_arena(16 * MEGABYTE); Term term = {0}; + static char *font_paths[] = { + "/usr/share/fonts/gofont/Go-Mono.ttf", + }; init_window(&term); + init_fonts(&term, font_paths, ARRAY_COUNT(font_paths), 64, &memory); f32 last_time = 0; while (!glfwWindowShouldClose(term.gl.window)) { diff --git a/os_unix.c b/os_unix.c @@ -9,6 +9,10 @@ typedef struct { size filesize; os_filetime timestamp; } os_file_stats; +typedef s8 os_mapped_file; + +#define OS_MAP_READ PROT_READ +#define OS_MAP_PRIVATE MAP_PRIVATE static Arena os_new_arena(size cap) @@ -55,3 +59,33 @@ os_read_file(Arena *a, char *name, size filesize) return text; } + +static os_mapped_file +os_map_file(char *path, i32 mode, i32 perm) +{ + os_mapped_file res = {0}; + + i32 open_mode = 0; + switch(mode) { + case OS_MAP_READ: open_mode = O_RDONLY; break; + default: ASSERT(0); + } + + i32 fd = open(path, open_mode); + os_file_stats fs = os_get_file_stats(path); + + ASSERT(fd > 0 && fs.filesize > 0); + + res.len = fs.filesize; + res.data = mmap(NULL, res.len, mode, perm, fd, 0); + ASSERT(res.data != MAP_FAILED); + close(fd); + + return res; +} + +static void +os_unmap_file(u8 *data, size len) +{ + munmap(data, len); +} diff --git a/util.c b/util.c @@ -27,7 +27,7 @@ __attribute((malloc, alloc_size(4, 2), alloc_align(3))) static void * alloc_(Arena *a, size len, size align, size count) { - size padding = -(uintptr_t)a->beg * (align - 1); + size padding = -(uintptr_t)a->beg & (align - 1); size available = a->end - a->beg - padding; ASSERT(available > 0 && count < available / len); diff --git a/util.h b/util.h @@ -19,7 +19,9 @@ #define MEGABYTE (1024ULL * 1024ULL) -#define ARRAY_COUNT(a) (sizeof(a) / sizeof(*a)) +#define ARRAY_COUNT(a) (sizeof(a) / sizeof(*a)) +#define BETWEEN(x, a, b) ((a) <= (x) && (x) <= (b)) +#define MAX(a, b) ((a) >= (b) ? (a) : (b)) typedef float f32; typedef double f64; @@ -36,6 +38,12 @@ typedef union { } iv2; typedef union { + struct { u32 x, y; }; + struct { u32 w, h; }; + u32 E[2]; +} uv2; + +typedef union { struct { f32 x, y; }; f32 E[2]; } v2; @@ -59,7 +67,10 @@ typedef struct { size len; u8 *data; } s8; #define GL_UNIFORMS \ X(Pmat) \ + X(charmap) \ X(texcolour) \ + X(texscale) \ + X(texslot) \ X(vertoff) \ X(vertscale) @@ -72,6 +83,8 @@ typedef struct { GLFWwindow *window; iv2 window_size; + f32 dt; + u32 vao, vbo; u32 glyph_tex; @@ -79,17 +92,53 @@ typedef struct { #define X(name) i32 name; union { struct { GL_UNIFORMS }; - i32 uniforms[4]; + i32 uniforms[7]; }; #undef X u32 flags; } GLCtx; +#include <ft2build.h> +#include FT_FREETYPE_H + +typedef struct { + u8 *buf; + i32 bufsize; + FT_Face face; +} Font; + +typedef struct { + Font *fonts; + u32 nfonts; + uv2 size; + i32 deltay; +} FontAtlas; + typedef struct { - GLCtx gl; + /* distance to shift glyph from bounding box origin */ + iv2 delta; + /* rendered glyph bitmap data (pixels) */ + uv2 size; +} Glyph; + +typedef struct { + GLCtx gl; + FontAtlas fa; + + u32 glyph_cache_len; + Glyph *glyph_cache; + FT_Library ftlib; } Term; #include "util.c" +#ifdef __unix__ +#include "os_unix.c" +#else +#error Unsupported Platform! +#endif + +#include "font.c" + #endif /* _UTIL_H_ */ diff --git a/vtgl.c b/vtgl.c @@ -6,6 +6,9 @@ #include "util.h" +#define MAX_FONT_SIZE 128 +#define TEXTURE_GLYPH_COUNT 128 + #define X(name) "u_"#name, static char *uniform_names[] = { GL_UNIFORMS }; #undef X @@ -42,27 +45,96 @@ resize(GLCtx *gl) } static void -update_uniforms(GLCtx *gl) +update_font_textures(Term *t) +{ + update_font_metrics(t); + + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D_ARRAY, t->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); + + for (u32 i = ' '; i <= '~'; i++) { + Glyph g; + u8 *bitmap = render(&t->fa, &g, i); + glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, + 0, 0, TEXTURE_GLYPH_COUNT - (i - ' ') - 1, + g.size.w, g.size.h, 1, + GL_RED, GL_UNSIGNED_BYTE, bitmap); + } +} + +static void +update_uniforms(Term *t) { - for (u32 i = 0; i < ARRAY_COUNT(gl->uniforms); i++) - gl->uniforms[i] = glGetUniformLocation(gl->program, uniform_names[i]); - gl->flags &= ~UPDATE_UNIFORMS; - resize(gl); + for (u32 i = 0; i < ARRAY_COUNT(t->gl.uniforms); i++) + t->gl.uniforms[i] = glGetUniformLocation(t->gl.program, uniform_names[i]); + t->gl.flags &= ~UPDATE_UNIFORMS; + + resize(&t->gl); + + glUniform1i(t->gl.texslot, 0); + update_font_textures(t); } static void draw_rectangle(GLCtx *gl, v2 pos, v2 size, u32 bg, u32 fg) { glUniform2uiv(gl->texcolour, 1, (u32 []){fg, bg}); - //glUniform1iv(glctx.u_charmap, count, charmap); - - //glUniform2fv(glctx.u_texscale, count, (f32 *)texscale); - glUniform2fv(gl->vertscale, 1, size.E); - glUniform2fv(gl->vertoff, 1, pos.E); + glUniform2fv(gl->vertscale, 1, size.E); + glUniform2fv(gl->vertoff, 1, pos.E); glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, 1); } +static void +draw_cell(Term *t, u32 cp, v2 pos, u32 bg, u32 fg) +{ + ASSERT(BETWEEN(cp, ' ', '~')); + i32 depth_idx = TEXTURE_GLYPH_COUNT - (cp - ' ') - 1; + Glyph g = t->glyph_cache[cp - ' ']; + + v2 texscale[2]; + texscale[0] = (v2){0}; + texscale[1] = (v2){ + .x = g.size.w / 128.0f, + .y = g.size.h / 128.0f, + }; + + + i32 charmap[2]; + charmap[0] = depth_idx; + charmap[1] = depth_idx; + + v2 vertscale[2]; + v2 vertoff[2]; + vertscale[0] = (v2){.x = 100, .y = 100}; + vertoff[0] = pos; + + v2 texture_position; + texture_position.x = (vertscale[0].x - g.size.w) * 0.5 + pos.x;// + g.delta.x; + texture_position.y = (vertscale[0].y - g.size.h) * 0.5 + pos.y;// + g.delta.y + t->fa.deltay; + + vertscale[1] = (v2){.x = g.size.w, .y = g.size.h}; + vertoff[1] = texture_position; + + u32 colours[4]; + colours[0] = fg; + colours[1] = bg; + colours[2] = fg; + colours[3] = bg; + + glUniform2uiv(t->gl.texcolour, 2, colours); + glUniform1iv(t->gl.charmap, 2, charmap); + glUniform2fv(t->gl.texscale, 2, (f32 *)texscale); + glUniform2fv(t->gl.vertscale, 2, (f32 *)vertscale); + glUniform2fv(t->gl.vertoff, 2, (f32 *)vertoff); + + glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, 2); +} + + /* NOTE: called when the window was resized */ static void fb_callback(GLFWwindow *win, i32 w, i32 h) @@ -94,7 +166,7 @@ DEBUG_EXPORT void do_terminal(Term *t, Arena a) { if (t->gl.flags & UPDATE_UNIFORMS) - update_uniforms(&t->gl); + update_uniforms(t); if (t->gl.flags & NEEDS_RESIZE) resize(&t->gl); @@ -127,6 +199,6 @@ do_terminal(Term *t, Arena a) } draw_rectangle(&t->gl, (v2){.x = 20, .y = 20}, (v2){.x = 200, .y = 100}, 0x7f0000ff, 0); - draw_rectangle(&t->gl, r2pos, r2size, 0x007f00ff, 0); + draw_cell(t, 'E', r2pos, 0x007f00ff, 0x000000ff); draw_rectangle(&t->gl, (v2){.x = 100, .y = 600}, (v2){.x = 50, .y = 100}, 0x00007fff, 0); }