vtgl

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

Commit: 7deeb70cf86620e1730ba033ae13625249e51849
Parent: 726eefc583ae3f8b971a69903f41ff16fe92777e
Author: Randy Palamar
Date:   Sat,  6 Jul 2024 12:32:06 -0600

push cells to CPU side framebuffer

Diffstat:
Mbuild.sh | 1+
Mmain.c | 2+-
Mos_unix.c | 54+++++++++++++++++++++++++++++++++++++++++++++++++++---
Mterminal.c | 164++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Mutil.h | 30+++++++++++++++++++++++-------
Mvtgl.c | 152+++++++++++++++++++++++++------------------------------------------------------
6 files changed, 275 insertions(+), 128 deletions(-)

diff --git a/build.sh b/build.sh @@ -7,6 +7,7 @@ ldflags="$ldflags -lfreetype $(pkg-config --static --libs glfw3 gl)" # Hot Reloading/Debugging cflags="$cflags -D_DEBUG -Wno-unused-function -Wno-undefined-internal" +#cflags="$cflags -fsanitize=address,undefined" libcflags="$cflags -fPIC -Wno-unused-function" libldflags="$ldflags -shared" diff --git a/main.c b/main.c @@ -264,7 +264,7 @@ check_shaders(GLCtx *gl, Arena a) } static void -line_buf_alloc(LineBuf *lb, Arena *a, u8 *start_position, CursorState state, size capacity) +line_buf_alloc(LineBuf *lb, Arena *a, u8 *start_position, CellStyle state, size capacity) { lb->cap = capacity; lb->filled = 0; diff --git a/os_unix.c b/os_unix.c @@ -127,6 +127,54 @@ os_alloc_ring_buffer(RingBuf *rb, size capacity) } static void +os_alloc_framebuffer(Framebuffer *fb, u32 rows, u32 cols) +{ + ASSERT(sizeof(Cell) == 16); + + size pagesize = sysconf(_SC_PAGESIZE); + + size fb_needed_space = rows * cols * sizeof(Cell); + if (fb->cells_alloc_size < fb_needed_space) { + if (fb_needed_space % pagesize != 0) + fb_needed_space += pagesize - fb_needed_space % pagesize; + + Cell *new = mmap(0, fb_needed_space, PROT_READ|PROT_WRITE, + MAP_ANONYMOUS|MAP_PRIVATE, -1, 0); + /* TODO: properly handle this case */ + ASSERT(new != MAP_FAILED); + + if (fb->cells) + munmap(fb->cells, fb->cells_alloc_size); + + fb->cells = new; + fb->cells_alloc_size = fb_needed_space; + } + + size rows_needed_space = rows * sizeof(Row); + if (fb->rows_alloc_size < rows_needed_space) { + if (rows_needed_space % pagesize != 0) + rows_needed_space += pagesize - rows_needed_space % pagesize; + + Row *new = mmap(0, rows_needed_space, PROT_READ|PROT_WRITE, + MAP_ANONYMOUS|MAP_PRIVATE, -1, 0); + /* TODO: properly handle this case */ + ASSERT(new != MAP_FAILED); + + if (fb->rows) + munmap(fb->rows, fb->rows_alloc_size); + + fb->rows = new; + fb->rows_alloc_size = rows_needed_space; + } + + fb->cells_count = rows * cols; + fb->rows_count = rows; + + for (u32 i = 0; i < fb->rows_count; i++) + fb->rows[i] = fb->cells + i * cols; +} + +static void execsh(char *defcmd) { char *sh; @@ -242,11 +290,11 @@ os_child_put_char(os_child c, u32 cp) } static void -os_set_term_size(os_child c, uv2 size, i32 x, i32 y) +os_set_term_size(os_child c, u32 rows, u32 cols, i32 x, i32 y) { struct winsize ws; - ws.ws_col = size.w; - ws.ws_row = size.h; + ws.ws_col = cols; + ws.ws_row = rows; ws.ws_xpixel = x; ws.ws_ypixel = y; if (ioctl(c.fd, TIOCSWINSZ, &ws) < 0) diff --git a/terminal.c b/terminal.c @@ -60,7 +60,7 @@ line_length(Line *l) } static void -feed_line(LineBuf *lb, u8 *position, CursorState cursor_state) +feed_line(LineBuf *lb, u8 *position, CellStyle cursor_state) { lb->buf[lb->widx++].end = position; lb->widx = lb->widx >= lb->cap ? 0 : lb->widx; @@ -73,6 +73,57 @@ feed_line(LineBuf *lb, u8 *position, CursorState cursor_state) } static void +fb_clear_region(Term *t, u32 r1, u32 r2, u32 c1, u32 c2) +{ + u32 tmp; + if (r1 > r2) { + tmp = r1; + r1 = r2; + r2 = tmp; + } + if (c1 > c2) { + tmp = c1; + c1 = c2; + c2 = tmp; + } + CLAMP(c1, 0, t->size.w - 1); + CLAMP(c2, 0, t->size.w - 1); + CLAMP(r1, 0, t->size.h - 1); + CLAMP(r2, 0, t->size.h - 1); + + for (u32 r = r1; r <= r2; r++) { + for (u32 c = c1; c <= c2; c++) { + t->fb.rows[r][c].style = t->cursor.state; + t->fb.rows[r][c].cp = ' '; + } + } +} + +static void +fb_scroll_down(Term *t, u32 top, u32 n) +{ + // TODO: CLAMP? + fb_clear_region(t, t->size.h - 1 - (n - 1), t->size.h - 1, 0, t->size.w); + for (u32 i = t->size.h - 1; i >= top + n; i--) { + Row tmp = t->fb.rows[i]; + t->fb.rows[i] = t->fb.rows[i - n]; + t->fb.rows[i - n] = tmp; + } +} + +static void +fb_scroll_up(Term *t, u32 top, u32 n) +{ + // TODO: CLAMP? + fb_clear_region(t, top, top + n - 1, 0, t->size.w); + for (u32 i = top; i < t->size.h - n; i++) { + Row tmp = t->fb.rows[i]; + t->fb.rows[i] = t->fb.rows[i + n]; + t->fb.rows[i + n] = tmp; + } +} + +static void cursor_reset(Term *t) { t->cursor.state.fg = (Colour){.rgba = 0x1e9e33ff}; @@ -98,6 +149,7 @@ cursor_step_column(Term *t, i32 step) if (t->cursor.row < t->size.h) return; t->cursor.row = t->size.h - 1; + fb_scroll_up(t, 0, 1); } static void @@ -131,15 +183,20 @@ dump_csi(CSI *csi) static void erase_in_display(Term *t, CSI *csi) { - switch(csi->argv[0]) { + Cursor *c = &t->cursor; + switch (csi->argv[0]) { case 0: /* Erase Below (default) */ - /* TODO: can we actually ignore this? */ + fb_clear_region(t, c->row, c->row, c->col, t->size.w); + if (c->row < t->size.h - 1) + fb_clear_region(t, c->row + 1, t->size.h, 0, t->size.w); break; case 1: /* Erase Above */ - t->gl.flags |= DISCARD_BUFFER; + if (c->row > 0) + fb_clear_region(t, 0, c->row - 1, 0, t->size.w); + fb_clear_region(t, c->row, c->row, 0, c->col); break; case 2: /* Erase All */ - t->gl.flags |= DISCARD_BUFFER; + fb_clear_region(t, 0, t->size.h, 0, t->size.w); break; case 3: /* Erase Saved Lines (xterm) */ /* NOTE: ignored; we don't save lines in the way xterm does */ @@ -155,13 +212,13 @@ erase_in_line(Term *t, CSI *csi) Cursor *c = &t->cursor; switch (csi->argv[0]) { case 0: /* Erase to Right */ - push_empty_cell_rect(t, c->row, c->row, c->col, t->size.w); + fb_clear_region(t, c->row, c->row, c->col, t->size.w); break; case 1: /* Erase to Left */ - push_empty_cell_rect(t, c->row, c->row, 0, c->col); + fb_clear_region(t, c->row, c->row, 0, c->col); break; case 2: /* Erase All */ - push_empty_cell_rect(t, c->row, c->row, 0, t->size.w); + fb_clear_region(t, c->row, c->row, 0, t->size.w); break; default: ASSERT(0); } @@ -193,7 +250,7 @@ set_mode(Term *t, CSI *csi, b32 set) static void set_colours(Term *t, CSI *csi) { - CursorState *cs = &t->cursor.state; + CellStyle *cs = &t->cursor.state; for (i32 i = 0; i < csi->argc; i++) { switch (csi->argv[i]) { case 0: cursor_reset(t); break; @@ -310,10 +367,12 @@ handle_escape(Term *t, s8 *raw) /* TODO: MODE_APPKEYPAD */ break; case 'M': /* RI -- Reverse Index */ - /* TODO: if the cursor is currently at the top of the terminal this - * needs to shift everything down by one row. Otherwise this just - * shifts the moves the cursor up one row */ - asm("nop"); + if (t->cursor.row == 0) { + fb_scroll_down(t, 0, 1); + } else { + cursor_move_to(t, t->cursor.row - 1, t->cursor.col); + result = HESC_CURSOR_MOVED; + } break; default: fprintf(stderr, "unknown escape sequence: ESC %c (0x%02x)\n", cp, cp); @@ -395,3 +454,82 @@ split_raw_input_to_lines(Term *t, s8 raw) t->unprocessed_bytes = 0; return parsed_lines; } + +static s8 +line_to_s8(Line *l) +{ + ASSERT(l->start <= l->end); + s8 result = {.len = l->end - l->start, .data = l->start}; + return result; +} + +static void +push_newline(Term *t) +{ + t->cursor.row++; + if (t->cursor.row >= t->size.h) { + t->cursor.row = t->size.h - 1; + fb_scroll_up(t, 0, 1); + } +} + +static void +push_tab(Term *t) +{ + u32 col = t->cursor.col; + u32 advance = g_tabstop - col % g_tabstop; + fb_clear_region(t, t->cursor.row, t->cursor.row, t->cursor.col, t->cursor.col + advance); + cursor_step_column(t, advance); +} + +static void +push_line(Term *t, Line *line) +{ + s8 l = line_to_s8(line); + + t->cursor.state = line->cursor_state; + + Cell *c; + while (l.len) { + /* TODO: handle unicode case */ + u32 cp = get_ascii(&l); + switch (cp) { + case 0x1B: handle_escape(t, &l); break; + case '\r': t->cursor.col = 0; break; + case '\n': push_newline(t); break; + case '\t': push_tab(t); break; + case '\b': + cursor_move_to(t, t->cursor.row, t->cursor.col - 1); + break; + default: + /* TODO properly make sure characters are printable */ + CLAMP(cp, ' ', '~'); + c = &t->fb.rows[t->cursor.row][t->cursor.col]; + c->cp = cp; + c->style = t->cursor.state; + /* TODO: properly advance cursor */ + cursor_step_column(t, 1); + } + } +} + +static void +blit_lines(Term *t) +{ + size line_count = t->size.h; + if (line_count > t->log_lines.filled) + line_count = t->log_lines.filled; + /* TODO: handle case where widx has wrapped around */ + ASSERT(t->log_lines.widx >= line_count); + size line_off = t->log_lines.widx - line_count; + + /* TODO: Performance!!! */ + fb_clear_region(t, 0, t->size.h, 0, t->size.w); + + /* NOTE: for now we assume that we blit the whole screen everytime */ + t->cursor.row = 0; + t->cursor.col = 0; + for (size i = 0; i <= line_count; i++) { + push_line(t, t->log_lines.buf + line_off + i); + } +} diff --git a/util.h b/util.h @@ -89,12 +89,29 @@ enum CellAttr { typedef struct { Colour fg, bg; - enum CellAttr attr; -} CursorState; + u32 attr; +} CellStyle; + +typedef struct { + u32 cp; + CellStyle style; +} Cell; + +typedef Cell *Row; + +typedef struct { + Cell *cells; + size cells_count; + size cells_alloc_size; + + Row *rows; + size rows_count; + size rows_alloc_size; +} Framebuffer; typedef struct { u32 row, col; - CursorState state; + CellStyle state; } Cursor; /* NOTE: virtual memory ring buffer */ @@ -108,7 +125,7 @@ typedef struct { typedef struct { u8 *start, *end; b32 has_unicode; - CursorState cursor_state; + CellStyle cursor_state; } Line; typedef struct { @@ -141,7 +158,6 @@ typedef struct { X(vertscale) enum gl_flags { - DISCARD_BUFFER = 1 << 0, UPDATE_RENDER_UNIFORMS = 1 << 29, UPDATE_POST_UNIFORMS = 1 << 30, }; @@ -217,6 +233,8 @@ typedef struct { LineBuf log_lines; size unprocessed_bytes; + Framebuffer fb; + os_child child; uv2 size; @@ -225,8 +243,6 @@ typedef struct { FT_Library ftlib; } Term; -static void push_empty_cell_rect(Term *, u32 minrow, u32 maxrow, u32 mincol, u32 maxcol); - #include "config.h" #include "font.c" #include "terminal.c" diff --git a/vtgl.c b/vtgl.c @@ -34,22 +34,11 @@ clear_colour(void) } static void -resize(GLCtx *gl) +resize(Term *t) { - v2 ws = gl->window_size; - - f32 pmat[4 * 4] = { - 2.0/ws.w, 0.0, 0.0, -1.0, - 0.0, 2.0/ws.h, 0.0, -1.0, - 0.0, 0.0, -1.0, 0.0, - 0.0, 0.0, 0.0, 1.0, - }; - - glViewport(0, 0, ws.w, ws.h); - glUseProgram(gl->programs[SHADER_RENDER]); - glUniformMatrix4fv(gl->render.Pmat, 1, GL_TRUE, pmat); - glUseProgram(gl->programs[SHADER_POST]); - glUniformMatrix4fv(gl->post.Pmat, 1, GL_TRUE, pmat); + v2 ws = t->gl.window_size; + os_alloc_framebuffer(&t->fb, t->size.h, t->size.w); + os_set_term_size(t->child, t->size.h, t->size.w, ws.w, ws.h); } static void @@ -181,11 +170,10 @@ draw_rectangle(GLCtx *gl, Rect r, Colour colour) } static void -push_cell(Term *t, u32 cp, Rect r, Colour bg, Colour fg) +push_cell(GLCtx *gl, Cell c, Rect r, f32 font_text_dy) { - GLCtx *gl = &t->gl; Glyph g; - i32 depth_idx = get_gpu_glyph_index(gl, cp, &g); + i32 depth_idx = get_gpu_glyph_index(gl, c.cp, &g); v2 texscale[2]; texscale[0] = (v2){0}; @@ -205,21 +193,19 @@ push_cell(Term *t, u32 cp, Rect r, Colour bg, Colour fg) v2 texture_position; texture_position.x = r.pos.x + g.delta.x; - texture_position.y = r.pos.y + g.delta.y + t->fa.deltay; + texture_position.y = r.pos.y + g.delta.y + font_text_dy; vertscale[1] = (v2){.x = g.size.w, .y = g.size.h}; vertoff[1] = texture_position; - u32 attr = t->cursor.state.attr; - - if (attr & ATTR_FAINT) - fg.a = 0.5 * 255; - u32 colours[4]; - colours[0] = (attr & ATTR_INVERSE)? bg.rgba : fg.rgba; - colours[1] = (attr & ATTR_INVERSE)? fg.rgba : bg.rgba; - colours[2] = (attr & ATTR_INVERSE)? bg.rgba : fg.rgba; - colours[3] = (attr & ATTR_INVERSE)? fg.rgba : bg.rgba; + colours[0] = (c.style.attr & ATTR_INVERSE)? c.style.bg.rgba : c.style.fg.rgba; + colours[1] = (c.style.attr & ATTR_INVERSE)? c.style.fg.rgba : c.style.bg.rgba; + colours[2] = (c.style.attr & ATTR_INVERSE)? c.style.bg.rgba : c.style.fg.rgba; + colours[3] = (c.style.attr & ATTR_INVERSE)? c.style.fg.rgba : c.style.bg.rgba; + + if (c.style.attr & ATTR_FAINT) + c.style.fg.a = 0.5 * 255; glUniform2uiv(gl->render.texcolour, 2, colours); glUniform1iv(gl->render.charmap, 2, charmap); @@ -230,79 +216,19 @@ push_cell(Term *t, u32 cp, Rect r, Colour bg, Colour fg) glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, 2); } -static s8 -line_to_s8(Line *l) -{ - ASSERT(l->start <= l->end); - s8 result = {.len = l->end - l->start, .data = l->start}; - return result; -} - static void -push_tab(Term *t) +render_framebuffer(Term *t) { - u32 col = t->cursor.col; - u32 advance = g_tabstop - col % g_tabstop; - cursor_step_column(t, advance); -} - -static void -push_line(Term *t, Line *line, v2 start_pos) -{ - s8 l = line_to_s8(line); - - t->cursor.state = line->cursor_state; - v2 cs = get_cell_size(t); - Rect cr = {.pos = start_pos, .size = cs}; - - while (l.len) { - /* TODO: handle unicode case */ - u32 cp = get_ascii(&l); - switch (cp) { - case 0x1B: handle_escape(t, &l); break; - case '\r': t->cursor.col = 0; break; - case '\n': t->cursor.row++; break; - case '\t': push_tab(t); break; - case '\b': - cursor_move_to(t, t->cursor.row, t->cursor.col - 1); - break; - default: - /* TODO properly make characters are printable */ - CLAMP(cp, ' ', '~'); - cr.pos.w = cs.w * t->cursor.col; - cr.pos.h = t->gl.window_size.h - (t->cursor.row + 1) * cs.h; - push_cell(t, cp, cr, t->cursor.state.bg, t->cursor.state.fg); - /* TODO: properly advance cursor */ - cursor_step_column(t, 1); - } + Rect cr = {.pos = { .y = t->gl.window_size.h - cs.h }, .size = cs}; - if (t->gl.flags & DISCARD_BUFFER) { - t->gl.flags &= ~DISCARD_BUFFER; - clear_colour(); + for (u32 r = 0; r < t->size.h; r++) { + for (u32 c = 0; c < t->size.w; c++) { + push_cell(&t->gl, t->fb.rows[r][c], cr, t->fa.deltay); + cr.pos.x += cs.w; } - } -} - -static void -blit_lines(Term *t) -{ - size line_count = t->size.h - 1; - CLAMP(line_count, 0, t->log_lines.filled); - v2 cs = get_cell_size(t); - /* TODO: handle case where widx has wrapped around */ - ASSERT(t->log_lines.widx >= line_count); - size line_off = t->log_lines.widx - line_count; - - /* NOTE: for now we assume that we blit the whole screen everytime */ - t->cursor.row = 0; - t->cursor.col = 0; - for (size i = 0; i <= line_count; i++) { - v2 pos = { - .x = t->cursor.col * cs.w, - .y = t->gl.window_size.h - (t->cursor.row + 1) * cs.h - }; - push_line(t, t->log_lines.buf + line_off + i, pos); + cr.pos.x = 0; + cr.pos.y -= cs.h; } } @@ -311,23 +237,36 @@ static void fb_callback(GLFWwindow *win, i32 w, i32 h) { Term *t = glfwGetWindowUserPointer(win); - t->gl.window_size.w = w; - t->gl.window_size.h = h; + t->gl.window_size = (v2){.w = w, .h = h}; v2 cs = get_cell_size(t); t->size.w = (u32)(w / cs.w); t->size.h = (u32)(h / cs.h); - /* NOTE: The terminal size still needs to have the correct number - * of rows so that terminal programs know how to size themselves. */ - os_set_term_size(t->child, (uv2){.w = t->size.w, .h = t->size.h}, w, h); + /* TODO: Proper padding */ + //v2 ws = {.x = t->size.w * cs.w, .y = t->size.h * cs.h}; + //f32 xpad = (w - ws.w) / 2; + //f32 ypad = (h - ws.h) / 2; + //glViewport(xpad, ypad, ws.w, ws.h); + glViewport(0, 0, w, h); glActiveTexture(GL_TEXTURE0 + t->gl.fb_tex_unit); glBindTexture(GL_TEXTURE_2D, t->gl.fb_tex); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, t->gl.window_size.w, t->gl.window_size.h, - 0, GL_RGBA, GL_UNSIGNED_BYTE, 0); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0); - resize(&t->gl); + f32 pmat[4 * 4] = { + 2.0/w, 0.0, 0.0, -1.0, + 0.0, 2.0/h, 0.0, -1.0, + 0.0, 0.0, -1.0, 0.0, + 0.0, 0.0, 0.0, 1.0, + }; + + glUseProgram(t->gl.programs[SHADER_RENDER]); + glUniformMatrix4fv(t->gl.render.Pmat, 1, GL_TRUE, pmat); + glUseProgram(t->gl.programs[SHADER_POST]); + glUniformMatrix4fv(t->gl.post.Pmat, 1, GL_TRUE, pmat); + + resize(t); } static void @@ -414,8 +353,11 @@ do_terminal(Term *t, Arena a) //glUseProgram(t->gl.programs[SHADER_RENDER]); glUniform1i(t->gl.render.texslot, 0); glBindFramebuffer(GL_FRAMEBUFFER, t->gl.fb); + clear_colour(); + blit_lines(t); + render_framebuffer(t); v2 cell_size = get_cell_size(t); v2 cursor_pos = { @@ -456,6 +398,8 @@ do_terminal(Term *t, Arena a) p_scale *= -1.0f; glBindFramebuffer(GL_FRAMEBUFFER, 0); + + clear_colour(); glUseProgram(t->gl.programs[SHADER_POST]); glUniform1i(t->gl.post.texslot, t->gl.fb_tex_unit); glUniform1f(t->gl.post.param, param);