From f8e8af1d781e7402829a310ceb05052900c76223 Mon Sep 17 00:00:00 2001 From: Daniel Hammer Date: Sat, 16 May 2026 09:33:30 +0200 Subject: [PATCH] feat: improve Terminal emulator performance --- kernel/src/Api/IoRedir.hpp | 4 +- programs/include/gui/terminal.hpp | 350 +++++++++++++------ programs/src/klog/main.cpp | 1 + programs/src/terminal/main.cpp | 3 + template/sysroot/include/gui/terminal.hpp | 392 ++++++++++++++++------ 5 files changed, 543 insertions(+), 207 deletions(-) diff --git a/kernel/src/Api/IoRedir.hpp b/kernel/src/Api/IoRedir.hpp index 2f5af9b..ee65fea 100644 --- a/kernel/src/Api/IoRedir.hpp +++ b/kernel/src/Api/IoRedir.hpp @@ -13,6 +13,8 @@ namespace Montauk { + static constexpr uint32_t RedirOutputStreamCapacity = 64 * 1024; + static int Sys_SpawnRedir(const char* path, const char* args) { char resolved[256]; if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return -1; @@ -28,7 +30,7 @@ namespace Montauk { return -1; } - Ipc::Stream* outStream = Ipc::CreateStream(); + Ipc::Stream* outStream = Ipc::CreateStream(RedirOutputStreamCapacity); Ipc::Stream* inStream = Ipc::CreateStream(); Ipc::Mailbox* keyMailbox = Ipc::CreateMailbox(); if (outStream == nullptr || inStream == nullptr || keyMailbox == nullptr) { diff --git a/programs/include/gui/terminal.hpp b/programs/include/gui/terminal.hpp index 0064cc5..2b33442 100644 --- a/programs/include/gui/terminal.hpp +++ b/programs/include/gui/terminal.hpp @@ -20,11 +20,14 @@ struct TermCell { }; static constexpr int TERM_MAX_SCROLLBACK = 500; -static constexpr int TERM_MAX_READS_PER_POLL = 8; +static constexpr int TERM_READ_BUF_SIZE = 8192; +static constexpr int TERM_MAX_DRAIN_BYTES_PER_POLL = 256 * 1024; +static constexpr int TERM_DRAIN_GRACE_YIELDS = 2; struct TerminalState { TermCell* cells; TermCell* alt_cells; // alternate screen buffer + TermCell* render_cells; // last visible cells painted into the window buffer int cols, rows; int cursor_x, cursor_y; int saved_cursor_x, saved_cursor_y; // saved cursor for alternate screen @@ -39,6 +42,15 @@ struct TerminalState { bool alt_screen_active; bool reverse_video; bool dirty; // true when content changed since last render + bool render_valid; + bool render_cursor_visible; + int render_cols, render_rows; + int render_term_cols, render_term_rows; + int render_base_row; + int render_pw, render_ph; + int render_cell_w, render_cell_h; + int render_cursor_x, render_cursor_y; + uint32_t* render_pixels; enum { STATE_NORMAL, STATE_ESC, STATE_CSI } parse_state; bool csi_private; // true if '?' was seen after CSI @@ -92,6 +104,24 @@ static inline TermCell* term_screen_row(TerminalState* t, int r) { return &t->cells[(t->scrollback_lines + r) * t->cols]; } +static inline bool term_color_equal(Color a, Color b) { + return a.r == b.r && a.g == b.g && a.b == b.b && a.a == b.a; +} + +static inline bool term_cell_equal(const TermCell& a, const TermCell& b) { + return a.ch == b.ch && term_color_equal(a.fg, b.fg) && term_color_equal(a.bg, b.bg); +} + +static inline void terminal_invalidate_render_cache(TerminalState* t) { + if (!t) return; + t->render_valid = false; + t->render_cursor_visible = false; +} + +static inline Color terminal_erase_bg(TerminalState* t) { + return t->current_bg; +} + static inline void terminal_scroll_up(TerminalState* t) { if (!t->alt_screen_active && t->scrollback_lines < t->max_scrollback) { // Room for scrollback: top visible row becomes scrollback, no data movement @@ -109,7 +139,7 @@ static inline void terminal_scroll_up(TerminalState* t) { // Clear the new bottom visible row TermCell* bottom = term_screen_row(t, t->rows - 1); for (int c = 0; c < t->cols; c++) { - bottom[c] = {' ', t->current_fg, colors::TERM_BG}; + bottom[c] = {' ', t->current_fg, terminal_erase_bg(t)}; } // Keep user's scrolled-back viewport stable @@ -138,6 +168,21 @@ static inline void terminal_init_cells(TerminalState* t, int cols, int rows, int t->alt_screen_active = false; t->reverse_video = false; t->dirty = true; + t->render_cells = nullptr; + t->render_valid = false; + t->render_cursor_visible = false; + t->render_cols = 0; + t->render_rows = 0; + t->render_term_cols = 0; + t->render_term_rows = 0; + t->render_base_row = 0; + t->render_pw = 0; + t->render_ph = 0; + t->render_cell_w = 0; + t->render_cell_h = 0; + t->render_cursor_x = 0; + t->render_cursor_y = 0; + t->render_pixels = nullptr; t->parse_state = TerminalState::STATE_NORMAL; t->csi_private = false; t->csi_param_count = 0; @@ -301,32 +346,33 @@ static inline void terminal_process_csi(TerminalState* t, char cmd) { } case 'J': { // Erase in display + Color erase_bg = terminal_erase_bg(t); if (p0 == 0) { // Clear from cursor to end TermCell* row = term_screen_row(t, t->cursor_y); for (int x = t->cursor_x; x < t->cols; x++) - row[x] = {' ', t->current_fg, colors::TERM_BG}; + row[x] = {' ', t->current_fg, erase_bg}; for (int r = t->cursor_y + 1; r < t->rows; r++) { TermCell* rp = term_screen_row(t, r); for (int c = 0; c < t->cols; c++) - rp[c] = {' ', t->current_fg, colors::TERM_BG}; + rp[c] = {' ', t->current_fg, erase_bg}; } } else if (p0 == 1) { // Clear from start to cursor for (int r = 0; r < t->cursor_y; r++) { TermCell* rp = term_screen_row(t, r); for (int c = 0; c < t->cols; c++) - rp[c] = {' ', t->current_fg, colors::TERM_BG}; + rp[c] = {' ', t->current_fg, erase_bg}; } TermCell* row = term_screen_row(t, t->cursor_y); for (int x = 0; x <= t->cursor_x; x++) - row[x] = {' ', t->current_fg, colors::TERM_BG}; + row[x] = {' ', t->current_fg, erase_bg}; } else if (p0 == 2) { // Clear entire screen for (int r = 0; r < t->rows; r++) { TermCell* rp = term_screen_row(t, r); for (int c = 0; c < t->cols; c++) - rp[c] = {' ', t->current_fg, colors::TERM_BG}; + rp[c] = {' ', t->current_fg, erase_bg}; } t->cursor_x = 0; t->cursor_y = 0; @@ -339,9 +385,10 @@ static inline void terminal_process_csi(TerminalState* t, char cmd) { if (p0 == 0) { start = t->cursor_x; end = t->cols; } else if (p0 == 1) { start = 0; end = t->cursor_x + 1; } else if (p0 == 2) { start = 0; end = t->cols; } + Color erase_bg = terminal_erase_bg(t); TermCell* row = term_screen_row(t, t->cursor_y); for (int x = start; x < end; x++) - row[x] = {' ', t->current_fg, colors::TERM_BG}; + row[x] = {' ', t->current_fg, erase_bg}; break; } case 'm': { @@ -520,122 +567,199 @@ static inline void terminal_feed(TerminalState* t, const char* data, int len) { } } +static inline void terminal_fill_pixel_rect(uint32_t* pixels, int pw, int ph, + int x, int y, int w, int h, + uint32_t color) { + if (!pixels || pw <= 0 || ph <= 0 || w <= 0 || h <= 0) return; + int x0 = x < 0 ? 0 : x; + int y0 = y < 0 ? 0 : y; + int x1 = x + w > pw ? pw : x + w; + int y1 = y + h > ph ? ph : y + h; + for (int row = y0; row < y1; row++) { + uint32_t* dst = &pixels[row * pw + x0]; + for (int col = x0; col < x1; col++) + *dst++ = color; + } +} + +static inline void terminal_draw_bitmap_char(uint32_t* pixels, int pw, int ph, + int x, int y, unsigned char ch, + uint32_t color) { + const uint8_t* glyph = &font_data[ch * FONT_HEIGHT]; + for (int fy = 0; fy < FONT_HEIGHT; fy++) { + int dy = y + fy; + if (dy >= ph) break; + if (dy < 0) continue; + uint8_t bits = glyph[fy]; + for (int fx = 0; fx < FONT_WIDTH; fx++) { + if (bits & (0x80 >> fx)) { + int dx = x + fx; + if (dx >= pw) break; + if (dx >= 0) pixels[dy * pw + dx] = color; + } + } + } +} + +static inline void terminal_draw_cell_pixels(const TermCell& cell, + uint32_t* pixels, int pw, int ph, + int px, int py, int cell_w, int cell_h, + bool fill_bg, bool use_ttf, + GlyphCache* gc) { + if (fill_bg) { + terminal_fill_pixel_rect(pixels, pw, ph, px, py, cell_w, cell_h, + cell.bg.to_pixel()); + } + + if (cell.ch <= 32 && cell.ch >= 0) return; + + if (use_ttf) { + int baseline = py + gc->ascent; + fonts::mono->draw_char_to_buffer(pixels, pw, ph, + px, baseline, (unsigned char)cell.ch, cell.fg, gc); + } else { + terminal_draw_bitmap_char(pixels, pw, ph, px, py, (unsigned char)cell.ch, + cell.fg.to_pixel()); + } +} + +static inline void terminal_draw_cursor_pixels(const TermCell& cell, + uint32_t* pixels, int pw, int ph, + int px, int py, int cell_w, int cell_h, + bool use_ttf, GlyphCache* gc) { + terminal_fill_pixel_rect(pixels, pw, ph, px, py, cell_w, cell_h, + colors::WHITE.to_pixel()); + + if (cell.ch <= 32 && cell.ch >= 0) return; + + if (use_ttf) { + int baseline = py + gc->ascent; + fonts::mono->draw_char_to_buffer(pixels, pw, ph, + px, baseline, (unsigned char)cell.ch, colors::BLACK, gc); + } else { + terminal_draw_bitmap_char(pixels, pw, ph, px, py, (unsigned char)cell.ch, + colors::BLACK.to_pixel()); + } +} + +static inline bool terminal_prepare_render_cache(TerminalState* t, + int visible_cols, + int visible_rows) { + if (visible_cols <= 0 || visible_rows <= 0) return false; + if (t->render_cells && t->render_cols == visible_cols && t->render_rows == visible_rows) + return true; + + if (t->render_cells) { + montauk::free(t->render_cells); + t->render_cells = nullptr; + } + + int cells = visible_cols * visible_rows; + t->render_cells = (TermCell*)montauk::alloc(cells * sizeof(TermCell)); + if (!t->render_cells) { + terminal_invalidate_render_cache(t); + t->render_cols = 0; + t->render_rows = 0; + return false; + } + + t->render_cols = visible_cols; + t->render_rows = visible_rows; + terminal_invalidate_render_cache(t); + return true; +} + +static inline TermCell terminal_visible_cell(TerminalState* t, int base_row, int x, int y) { + return t->cells[(base_row + y) * t->cols + x]; +} + static inline void terminal_render(TerminalState* t, uint32_t* pixels, int pw, int ph) { - if (!t->dirty || !t->cells) return; - t->dirty = false; + if (!t->dirty || !t->cells || !pixels || pw <= 0 || ph <= 0) return; int cell_w = mono_cell_width(); int cell_h = mono_cell_height(); + if (cell_w <= 0 || cell_h <= 0) return; + bool use_ttf = fonts::mono && fonts::mono->valid; GlyphCache* gc = use_ttf ? fonts::mono->get_cache(fonts::TERM_SIZE) : nullptr; - // Fill background using row-copy: fill first row, then memcpy to the rest - uint32_t bg_px = colors::TERM_BG.to_pixel(); - int row_bytes = pw * sizeof(uint32_t); - for (int i = 0; i < pw; i++) pixels[i] = bg_px; - for (int r = 1; r < ph; r++) { - montauk::memcpy(&pixels[r * pw], pixels, row_bytes); - } - - // Determine which rows of the buffer to display int base_row = t->scrollback_lines - t->view_offset; if (base_row < 0) base_row = 0; - // Render each visible cell int visible_rows = ph / cell_h; int visible_cols = pw / cell_w; if (visible_rows > t->rows) visible_rows = t->rows; if (visible_cols > t->cols) visible_cols = t->cols; + if (visible_rows <= 0 || visible_cols <= 0) return; + + bool have_cache = terminal_prepare_render_cache(t, visible_cols, visible_rows); + bool full_redraw = !have_cache || !t->render_valid || + t->render_pixels != pixels || + t->render_pw != pw || t->render_ph != ph || + t->render_cell_w != cell_w || t->render_cell_h != cell_h || + t->render_base_row != base_row || + t->render_term_cols != t->cols || t->render_term_rows != t->rows; + + uint32_t bg_px = colors::TERM_BG.to_pixel(); + if (full_redraw) { + int row_bytes = pw * sizeof(uint32_t); + for (int i = 0; i < pw; i++) pixels[i] = bg_px; + for (int r = 1; r < ph; r++) + montauk::memcpy(&pixels[r * pw], pixels, row_bytes); + } else if (t->render_cursor_visible) { + int x = t->render_cursor_x; + int y = t->render_cursor_y; + if (x >= 0 && x < visible_cols && y >= 0 && y < visible_rows) { + TermCell cell = terminal_visible_cell(t, base_row, x, y); + terminal_draw_cell_pixels(cell, pixels, pw, ph, + x * cell_w, y * cell_h, + cell_w, cell_h, true, use_ttf, gc); + if (have_cache) + t->render_cells[y * visible_cols + x] = cell; + } + } for (int r = 0; r < visible_rows; r++) { int py = r * cell_h; - int src_row = base_row + r; for (int c = 0; c < visible_cols; c++) { - int idx = src_row * t->cols + c; - TermCell& cell = t->cells[idx]; + TermCell cell = terminal_visible_cell(t, base_row, c, r); + int cache_idx = r * visible_cols + c; + bool changed = full_redraw || !have_cache || + !term_cell_equal(t->render_cells[cache_idx], cell); + if (!changed) continue; - int px = c * cell_w; - - // Only draw cell background if it differs from terminal bg - uint32_t cell_bg = cell.bg.to_pixel(); - if (cell_bg != bg_px) { - for (int fy = 0; fy < cell_h && py + fy < ph; fy++) { - uint32_t* row = &pixels[(py + fy) * pw + px]; - for (int fx = 0; fx < cell_w && px + fx < pw; fx++) { - row[fx] = cell_bg; - } - } - } - - // Draw character glyph - if (cell.ch > 32 || cell.ch < 0) { - if (use_ttf) { - int baseline = py + gc->ascent; - fonts::mono->draw_char_to_buffer(pixels, pw, ph, - px, baseline, (unsigned char)cell.ch, cell.fg, gc); - } else { - uint32_t cell_fg = cell.fg.to_pixel(); - const uint8_t* glyph = &font_data[(unsigned char)cell.ch * FONT_HEIGHT]; - for (int fy = 0; fy < FONT_HEIGHT; fy++) { - int dy = py + fy; - if (dy >= ph) break; - uint8_t bits = glyph[fy]; - for (int fx = 0; fx < FONT_WIDTH; fx++) { - if (bits & (0x80 >> fx)) { - int dx = px + fx; - if (dx >= pw) break; - pixels[dy * pw + dx] = cell_fg; - } - } - } - } - } + bool fill_bg = !full_redraw || cell.bg.to_pixel() != bg_px; + terminal_draw_cell_pixels(cell, pixels, pw, ph, + c * cell_w, py, + cell_w, cell_h, fill_bg, use_ttf, gc); + if (have_cache) + t->render_cells[cache_idx] = cell; } } - // Draw cursor (only when viewing live position) - if (t->view_offset == 0 && t->cursor_visible && - t->cursor_x < visible_cols && t->cursor_y < visible_rows) { - int cx = t->cursor_x * cell_w; - int cy = t->cursor_y * cell_h; - uint32_t cursor_px = colors::WHITE.to_pixel(); - for (int fy = 0; fy < cell_h; fy++) { - int dy = cy + fy; - if (dy >= ph) break; - for (int fx = 0; fx < cell_w; fx++) { - int dx = cx + fx; - if (dx >= pw) break; - pixels[dy * pw + dx] = cursor_px; - } - } - // Draw character on top of cursor in black - if (t->cursor_y < t->rows && t->cursor_x < t->cols) { - TermCell* row = term_screen_row(t, t->cursor_y); - char ch = row[t->cursor_x].ch; - if (ch > 32 || ch < 0) { - if (use_ttf) { - int baseline = cy + gc->ascent; - fonts::mono->draw_char_to_buffer(pixels, pw, ph, - cx, baseline, (unsigned char)ch, colors::BLACK, gc); - } else { - const uint8_t* glyph = &font_data[(unsigned char)ch * FONT_HEIGHT]; - uint32_t black_px = colors::BLACK.to_pixel(); - for (int fy = 0; fy < FONT_HEIGHT; fy++) { - int dy = cy + fy; - if (dy >= ph) break; - uint8_t bits = glyph[fy]; - for (int fx = 0; fx < FONT_WIDTH; fx++) { - if (bits & (0x80 >> fx)) { - int dx = cx + fx; - if (dx >= pw) break; - pixels[dy * pw + dx] = black_px; - } - } - } - } - } - } + bool cursor_now_visible = t->view_offset == 0 && t->cursor_visible && + t->cursor_x >= 0 && t->cursor_y >= 0 && + t->cursor_x < visible_cols && t->cursor_y < visible_rows; + if (cursor_now_visible) { + TermCell cell = terminal_visible_cell(t, base_row, t->cursor_x, t->cursor_y); + terminal_draw_cursor_pixels(cell, pixels, pw, ph, + t->cursor_x * cell_w, t->cursor_y * cell_h, + cell_w, cell_h, use_ttf, gc); } + + t->render_valid = have_cache; + t->render_cursor_visible = cursor_now_visible; + t->render_cursor_x = t->cursor_x; + t->render_cursor_y = t->cursor_y; + t->render_term_cols = t->cols; + t->render_term_rows = t->rows; + t->render_base_row = base_row; + t->render_pw = pw; + t->render_ph = ph; + t->render_cell_w = cell_w; + t->render_cell_h = cell_h; + t->render_pixels = pixels; + t->dirty = false; } static inline void terminal_resize(TerminalState* t, int new_cols, int new_rows) { @@ -683,6 +807,10 @@ static inline void terminal_resize(TerminalState* t, int new_cols, int new_rows) if (t->cells) montauk::free(t->cells); if (t->alt_cells) montauk::free(t->alt_cells); + if (t->render_cells) { + montauk::free(t->render_cells); + t->render_cells = nullptr; + } t->cells = new_cells; t->alt_cells = new_alt; @@ -691,6 +819,7 @@ static inline void terminal_resize(TerminalState* t, int new_cols, int new_rows) t->scrollback_lines = new_scrollback; t->cursor_x = new_cursor_x; t->cursor_y = new_cursor_y; + terminal_invalidate_render_cache(t); // Clamp view offset if (t->view_offset > t->scrollback_lines) @@ -716,15 +845,28 @@ static inline void terminal_handle_key(TerminalState* t, const Montauk::KeyEvent // Returns false if the child process has exited static inline bool terminal_poll(TerminalState* t) { if (t->child_pid <= 0) return false; - char buf[4096]; - // Drain a bounded amount per frame so window close/input events stay responsive. - for (int reads = 0; reads < TERM_MAX_READS_PER_POLL; reads++) { + char buf[TERM_READ_BUF_SIZE]; + int drained = 0; + int grace_yields = 0; + + // Drain bursty TUI output as one frame. When a child was blocked on a full + // stream, a short yield lets it finish the redraw before we present. + for (;;) { int n = montauk::childio_read(t->child_pid, buf, sizeof(buf)); if (n > 0) { terminal_feed(t, buf, n); + drained += n; + grace_yields = 0; + if (drained >= TERM_MAX_DRAIN_BYTES_PER_POLL) + break; } else { // n == -1 means child process is gone; n == 0 means no data yet if (n < 0) { t->child_pid = 0; return false; } + if (drained > 0 && grace_yields < TERM_DRAIN_GRACE_YIELDS) { + grace_yields++; + montauk::yield(); + continue; + } break; } } diff --git a/programs/src/klog/main.cpp b/programs/src/klog/main.cpp index c4e8cfd..2322501 100644 --- a/programs/src/klog/main.cpp +++ b/programs/src/klog/main.cpp @@ -148,6 +148,7 @@ static void klog_handle_mouse(const Montauk::WinEvent& ev) { static void klog_cleanup() { if (g_klog.term.cells) montauk::free(g_klog.term.cells); if (g_klog.term.alt_cells) montauk::free(g_klog.term.alt_cells); + if (g_klog.term.render_cells) montauk::free(g_klog.term.render_cells); if (g_klog.klog_buf) montauk::mfree(g_klog.klog_buf); montauk::memset(&g_klog, 0, sizeof(g_klog)); } diff --git a/programs/src/terminal/main.cpp b/programs/src/terminal/main.cpp index 38b4b5c..33bd112 100644 --- a/programs/src/terminal/main.cpp +++ b/programs/src/terminal/main.cpp @@ -93,6 +93,7 @@ static void term_free_tab(TerminalState* ts) { if (ts->child_pid > 0) term_kill_process_tree(ts->child_pid); if (ts->cells) montauk::free(ts->cells); if (ts->alt_cells) montauk::free(ts->alt_cells); + if (ts->render_cells) montauk::free(ts->render_cells); montauk::mfree(ts); } @@ -133,6 +134,7 @@ static void term_close_tab(int idx) { if (g_tabs.active_tab >= g_tabs.tab_count) g_tabs.active_tab = g_tabs.tab_count - 1; + terminal_invalidate_render_cache(g_tabs.tabs[g_tabs.active_tab]); g_tabs.tabs[g_tabs.active_tab]->dirty = true; term_request_redraw(); } @@ -192,6 +194,7 @@ static bool term_render() { } if (g_tabs.active_tab != g_tabs.prev_active_tab) { + terminal_invalidate_render_cache(g_tabs.tabs[g_tabs.active_tab]); g_tabs.tabs[g_tabs.active_tab]->dirty = true; g_tabs.prev_active_tab = g_tabs.active_tab; } diff --git a/template/sysroot/include/gui/terminal.hpp b/template/sysroot/include/gui/terminal.hpp index 65c1a54..2b33442 100644 --- a/template/sysroot/include/gui/terminal.hpp +++ b/template/sysroot/include/gui/terminal.hpp @@ -20,10 +20,14 @@ struct TermCell { }; static constexpr int TERM_MAX_SCROLLBACK = 500; +static constexpr int TERM_READ_BUF_SIZE = 8192; +static constexpr int TERM_MAX_DRAIN_BYTES_PER_POLL = 256 * 1024; +static constexpr int TERM_DRAIN_GRACE_YIELDS = 2; struct TerminalState { TermCell* cells; TermCell* alt_cells; // alternate screen buffer + TermCell* render_cells; // last visible cells painted into the window buffer int cols, rows; int cursor_x, cursor_y; int saved_cursor_x, saved_cursor_y; // saved cursor for alternate screen @@ -38,6 +42,15 @@ struct TerminalState { bool alt_screen_active; bool reverse_video; bool dirty; // true when content changed since last render + bool render_valid; + bool render_cursor_visible; + int render_cols, render_rows; + int render_term_cols, render_term_rows; + int render_base_row; + int render_pw, render_ph; + int render_cell_w, render_cell_h; + int render_cursor_x, render_cursor_y; + uint32_t* render_pixels; enum { STATE_NORMAL, STATE_ESC, STATE_CSI } parse_state; bool csi_private; // true if '?' was seen after CSI @@ -69,11 +82,46 @@ static inline Color term_ansi_color(int idx) { } } +// ANSI 256-color palette (0-15 = standard, 16-231 = RGB cube, 232-255 = grayscale) +static inline Color term_ansi_256_color(int idx) { + if (idx < 0) return colors::TERM_FG; + if (idx <= 15) return term_ansi_color(idx); + if (idx <= 231) { + // 6x6x6 RGB cube: idx 16 = (0,0,0), idx 231 = (5,5,5) + int r = ((idx - 16) / 36) * 51; + int g = (((idx - 16) % 36) / 6) * 51; + int b = ((idx - 16) % 6) * 51; + return Color::from_rgb(r, g, b); + } + // Grayscale: idx 232 = #080808, idx 255 = #eeeeee + int gray = 8 + (idx - 232) * 10; + if (gray > 238) gray = 238; + return Color::from_rgb(gray, gray, gray); +} + // Helper: pointer to start of screen-relative row r in the cells buffer static inline TermCell* term_screen_row(TerminalState* t, int r) { return &t->cells[(t->scrollback_lines + r) * t->cols]; } +static inline bool term_color_equal(Color a, Color b) { + return a.r == b.r && a.g == b.g && a.b == b.b && a.a == b.a; +} + +static inline bool term_cell_equal(const TermCell& a, const TermCell& b) { + return a.ch == b.ch && term_color_equal(a.fg, b.fg) && term_color_equal(a.bg, b.bg); +} + +static inline void terminal_invalidate_render_cache(TerminalState* t) { + if (!t) return; + t->render_valid = false; + t->render_cursor_visible = false; +} + +static inline Color terminal_erase_bg(TerminalState* t) { + return t->current_bg; +} + static inline void terminal_scroll_up(TerminalState* t) { if (!t->alt_screen_active && t->scrollback_lines < t->max_scrollback) { // Room for scrollback: top visible row becomes scrollback, no data movement @@ -91,7 +139,7 @@ static inline void terminal_scroll_up(TerminalState* t) { // Clear the new bottom visible row TermCell* bottom = term_screen_row(t, t->rows - 1); for (int c = 0; c < t->cols; c++) { - bottom[c] = {' ', t->current_fg, colors::TERM_BG}; + bottom[c] = {' ', t->current_fg, terminal_erase_bg(t)}; } // Keep user's scrolled-back viewport stable @@ -120,6 +168,21 @@ static inline void terminal_init_cells(TerminalState* t, int cols, int rows, int t->alt_screen_active = false; t->reverse_video = false; t->dirty = true; + t->render_cells = nullptr; + t->render_valid = false; + t->render_cursor_visible = false; + t->render_cols = 0; + t->render_rows = 0; + t->render_term_cols = 0; + t->render_term_rows = 0; + t->render_base_row = 0; + t->render_pw = 0; + t->render_ph = 0; + t->render_cell_w = 0; + t->render_cell_h = 0; + t->render_cursor_x = 0; + t->render_cursor_y = 0; + t->render_pixels = nullptr; t->parse_state = TerminalState::STATE_NORMAL; t->csi_private = false; t->csi_param_count = 0; @@ -283,32 +346,33 @@ static inline void terminal_process_csi(TerminalState* t, char cmd) { } case 'J': { // Erase in display + Color erase_bg = terminal_erase_bg(t); if (p0 == 0) { // Clear from cursor to end TermCell* row = term_screen_row(t, t->cursor_y); for (int x = t->cursor_x; x < t->cols; x++) - row[x] = {' ', t->current_fg, colors::TERM_BG}; + row[x] = {' ', t->current_fg, erase_bg}; for (int r = t->cursor_y + 1; r < t->rows; r++) { TermCell* rp = term_screen_row(t, r); for (int c = 0; c < t->cols; c++) - rp[c] = {' ', t->current_fg, colors::TERM_BG}; + rp[c] = {' ', t->current_fg, erase_bg}; } } else if (p0 == 1) { // Clear from start to cursor for (int r = 0; r < t->cursor_y; r++) { TermCell* rp = term_screen_row(t, r); for (int c = 0; c < t->cols; c++) - rp[c] = {' ', t->current_fg, colors::TERM_BG}; + rp[c] = {' ', t->current_fg, erase_bg}; } TermCell* row = term_screen_row(t, t->cursor_y); for (int x = 0; x <= t->cursor_x; x++) - row[x] = {' ', t->current_fg, colors::TERM_BG}; + row[x] = {' ', t->current_fg, erase_bg}; } else if (p0 == 2) { // Clear entire screen for (int r = 0; r < t->rows; r++) { TermCell* rp = term_screen_row(t, r); for (int c = 0; c < t->cols; c++) - rp[c] = {' ', t->current_fg, colors::TERM_BG}; + rp[c] = {' ', t->current_fg, erase_bg}; } t->cursor_x = 0; t->cursor_y = 0; @@ -321,15 +385,44 @@ static inline void terminal_process_csi(TerminalState* t, char cmd) { if (p0 == 0) { start = t->cursor_x; end = t->cols; } else if (p0 == 1) { start = 0; end = t->cursor_x + 1; } else if (p0 == 2) { start = 0; end = t->cols; } + Color erase_bg = terminal_erase_bg(t); TermCell* row = term_screen_row(t, t->cursor_y); for (int x = start; x < end; x++) - row[x] = {' ', t->current_fg, colors::TERM_BG}; + row[x] = {' ', t->current_fg, erase_bg}; break; } case 'm': { // SGR - Set Graphics Rendition for (int i = 0; i < t->csi_param_count; i++) { int code = t->csi_params[i]; + + // Handle extended color sequences: 38;5;N or 48;5;N or 38;2;R;G;B or 48;2;R;G;B + if ((code == 38 || code == 48) && i + 2 < t->csi_param_count) { + if (t->csi_params[i + 1] == 5) { + // 256-color: 38;5;N or 48;5;N + int color_idx = t->csi_params[i + 2]; + if (code == 38) { + t->current_fg = term_ansi_256_color(color_idx); + } else { + t->current_bg = term_ansi_256_color(color_idx); + } + i += 2; + continue; + } else if (t->csi_params[i + 1] == 2 && i + 4 < t->csi_param_count) { + // Truecolor: 38;2;R;G;B or 48;2;R;G;B + int r = t->csi_params[i + 2]; + int g = t->csi_params[i + 3]; + int b = t->csi_params[i + 4]; + if (code == 38) { + t->current_fg = Color::from_rgb(r, g, b); + } else { + t->current_bg = Color::from_rgb(r, g, b); + } + i += 4; + continue; + } + } + if (code == 0) { t->current_fg = colors::TERM_FG; t->current_bg = colors::TERM_BG; @@ -474,122 +567,199 @@ static inline void terminal_feed(TerminalState* t, const char* data, int len) { } } +static inline void terminal_fill_pixel_rect(uint32_t* pixels, int pw, int ph, + int x, int y, int w, int h, + uint32_t color) { + if (!pixels || pw <= 0 || ph <= 0 || w <= 0 || h <= 0) return; + int x0 = x < 0 ? 0 : x; + int y0 = y < 0 ? 0 : y; + int x1 = x + w > pw ? pw : x + w; + int y1 = y + h > ph ? ph : y + h; + for (int row = y0; row < y1; row++) { + uint32_t* dst = &pixels[row * pw + x0]; + for (int col = x0; col < x1; col++) + *dst++ = color; + } +} + +static inline void terminal_draw_bitmap_char(uint32_t* pixels, int pw, int ph, + int x, int y, unsigned char ch, + uint32_t color) { + const uint8_t* glyph = &font_data[ch * FONT_HEIGHT]; + for (int fy = 0; fy < FONT_HEIGHT; fy++) { + int dy = y + fy; + if (dy >= ph) break; + if (dy < 0) continue; + uint8_t bits = glyph[fy]; + for (int fx = 0; fx < FONT_WIDTH; fx++) { + if (bits & (0x80 >> fx)) { + int dx = x + fx; + if (dx >= pw) break; + if (dx >= 0) pixels[dy * pw + dx] = color; + } + } + } +} + +static inline void terminal_draw_cell_pixels(const TermCell& cell, + uint32_t* pixels, int pw, int ph, + int px, int py, int cell_w, int cell_h, + bool fill_bg, bool use_ttf, + GlyphCache* gc) { + if (fill_bg) { + terminal_fill_pixel_rect(pixels, pw, ph, px, py, cell_w, cell_h, + cell.bg.to_pixel()); + } + + if (cell.ch <= 32 && cell.ch >= 0) return; + + if (use_ttf) { + int baseline = py + gc->ascent; + fonts::mono->draw_char_to_buffer(pixels, pw, ph, + px, baseline, (unsigned char)cell.ch, cell.fg, gc); + } else { + terminal_draw_bitmap_char(pixels, pw, ph, px, py, (unsigned char)cell.ch, + cell.fg.to_pixel()); + } +} + +static inline void terminal_draw_cursor_pixels(const TermCell& cell, + uint32_t* pixels, int pw, int ph, + int px, int py, int cell_w, int cell_h, + bool use_ttf, GlyphCache* gc) { + terminal_fill_pixel_rect(pixels, pw, ph, px, py, cell_w, cell_h, + colors::WHITE.to_pixel()); + + if (cell.ch <= 32 && cell.ch >= 0) return; + + if (use_ttf) { + int baseline = py + gc->ascent; + fonts::mono->draw_char_to_buffer(pixels, pw, ph, + px, baseline, (unsigned char)cell.ch, colors::BLACK, gc); + } else { + terminal_draw_bitmap_char(pixels, pw, ph, px, py, (unsigned char)cell.ch, + colors::BLACK.to_pixel()); + } +} + +static inline bool terminal_prepare_render_cache(TerminalState* t, + int visible_cols, + int visible_rows) { + if (visible_cols <= 0 || visible_rows <= 0) return false; + if (t->render_cells && t->render_cols == visible_cols && t->render_rows == visible_rows) + return true; + + if (t->render_cells) { + montauk::free(t->render_cells); + t->render_cells = nullptr; + } + + int cells = visible_cols * visible_rows; + t->render_cells = (TermCell*)montauk::alloc(cells * sizeof(TermCell)); + if (!t->render_cells) { + terminal_invalidate_render_cache(t); + t->render_cols = 0; + t->render_rows = 0; + return false; + } + + t->render_cols = visible_cols; + t->render_rows = visible_rows; + terminal_invalidate_render_cache(t); + return true; +} + +static inline TermCell terminal_visible_cell(TerminalState* t, int base_row, int x, int y) { + return t->cells[(base_row + y) * t->cols + x]; +} + static inline void terminal_render(TerminalState* t, uint32_t* pixels, int pw, int ph) { - if (!t->dirty || !t->cells) return; - t->dirty = false; + if (!t->dirty || !t->cells || !pixels || pw <= 0 || ph <= 0) return; int cell_w = mono_cell_width(); int cell_h = mono_cell_height(); + if (cell_w <= 0 || cell_h <= 0) return; + bool use_ttf = fonts::mono && fonts::mono->valid; GlyphCache* gc = use_ttf ? fonts::mono->get_cache(fonts::TERM_SIZE) : nullptr; - // Fill background using row-copy: fill first row, then memcpy to the rest - uint32_t bg_px = colors::TERM_BG.to_pixel(); - int row_bytes = pw * sizeof(uint32_t); - for (int i = 0; i < pw; i++) pixels[i] = bg_px; - for (int r = 1; r < ph; r++) { - montauk::memcpy(&pixels[r * pw], pixels, row_bytes); - } - - // Determine which rows of the buffer to display int base_row = t->scrollback_lines - t->view_offset; if (base_row < 0) base_row = 0; - // Render each visible cell int visible_rows = ph / cell_h; int visible_cols = pw / cell_w; if (visible_rows > t->rows) visible_rows = t->rows; if (visible_cols > t->cols) visible_cols = t->cols; + if (visible_rows <= 0 || visible_cols <= 0) return; + + bool have_cache = terminal_prepare_render_cache(t, visible_cols, visible_rows); + bool full_redraw = !have_cache || !t->render_valid || + t->render_pixels != pixels || + t->render_pw != pw || t->render_ph != ph || + t->render_cell_w != cell_w || t->render_cell_h != cell_h || + t->render_base_row != base_row || + t->render_term_cols != t->cols || t->render_term_rows != t->rows; + + uint32_t bg_px = colors::TERM_BG.to_pixel(); + if (full_redraw) { + int row_bytes = pw * sizeof(uint32_t); + for (int i = 0; i < pw; i++) pixels[i] = bg_px; + for (int r = 1; r < ph; r++) + montauk::memcpy(&pixels[r * pw], pixels, row_bytes); + } else if (t->render_cursor_visible) { + int x = t->render_cursor_x; + int y = t->render_cursor_y; + if (x >= 0 && x < visible_cols && y >= 0 && y < visible_rows) { + TermCell cell = terminal_visible_cell(t, base_row, x, y); + terminal_draw_cell_pixels(cell, pixels, pw, ph, + x * cell_w, y * cell_h, + cell_w, cell_h, true, use_ttf, gc); + if (have_cache) + t->render_cells[y * visible_cols + x] = cell; + } + } for (int r = 0; r < visible_rows; r++) { int py = r * cell_h; - int src_row = base_row + r; for (int c = 0; c < visible_cols; c++) { - int idx = src_row * t->cols + c; - TermCell& cell = t->cells[idx]; + TermCell cell = terminal_visible_cell(t, base_row, c, r); + int cache_idx = r * visible_cols + c; + bool changed = full_redraw || !have_cache || + !term_cell_equal(t->render_cells[cache_idx], cell); + if (!changed) continue; - int px = c * cell_w; - - // Only draw cell background if it differs from terminal bg - uint32_t cell_bg = cell.bg.to_pixel(); - if (cell_bg != bg_px) { - for (int fy = 0; fy < cell_h && py + fy < ph; fy++) { - uint32_t* row = &pixels[(py + fy) * pw + px]; - for (int fx = 0; fx < cell_w && px + fx < pw; fx++) { - row[fx] = cell_bg; - } - } - } - - // Draw character glyph - if (cell.ch > 32 || cell.ch < 0) { - if (use_ttf) { - int baseline = py + gc->ascent; - fonts::mono->draw_char_to_buffer(pixels, pw, ph, - px, baseline, (unsigned char)cell.ch, cell.fg, gc); - } else { - uint32_t cell_fg = cell.fg.to_pixel(); - const uint8_t* glyph = &font_data[(unsigned char)cell.ch * FONT_HEIGHT]; - for (int fy = 0; fy < FONT_HEIGHT; fy++) { - int dy = py + fy; - if (dy >= ph) break; - uint8_t bits = glyph[fy]; - for (int fx = 0; fx < FONT_WIDTH; fx++) { - if (bits & (0x80 >> fx)) { - int dx = px + fx; - if (dx >= pw) break; - pixels[dy * pw + dx] = cell_fg; - } - } - } - } - } + bool fill_bg = !full_redraw || cell.bg.to_pixel() != bg_px; + terminal_draw_cell_pixels(cell, pixels, pw, ph, + c * cell_w, py, + cell_w, cell_h, fill_bg, use_ttf, gc); + if (have_cache) + t->render_cells[cache_idx] = cell; } } - // Draw cursor (only when viewing live position) - if (t->view_offset == 0 && t->cursor_visible && - t->cursor_x < visible_cols && t->cursor_y < visible_rows) { - int cx = t->cursor_x * cell_w; - int cy = t->cursor_y * cell_h; - uint32_t cursor_px = colors::WHITE.to_pixel(); - for (int fy = 0; fy < cell_h; fy++) { - int dy = cy + fy; - if (dy >= ph) break; - for (int fx = 0; fx < cell_w; fx++) { - int dx = cx + fx; - if (dx >= pw) break; - pixels[dy * pw + dx] = cursor_px; - } - } - // Draw character on top of cursor in black - if (t->cursor_y < t->rows && t->cursor_x < t->cols) { - TermCell* row = term_screen_row(t, t->cursor_y); - char ch = row[t->cursor_x].ch; - if (ch > 32 || ch < 0) { - if (use_ttf) { - int baseline = cy + gc->ascent; - fonts::mono->draw_char_to_buffer(pixels, pw, ph, - cx, baseline, (unsigned char)ch, colors::BLACK, gc); - } else { - const uint8_t* glyph = &font_data[(unsigned char)ch * FONT_HEIGHT]; - uint32_t black_px = colors::BLACK.to_pixel(); - for (int fy = 0; fy < FONT_HEIGHT; fy++) { - int dy = cy + fy; - if (dy >= ph) break; - uint8_t bits = glyph[fy]; - for (int fx = 0; fx < FONT_WIDTH; fx++) { - if (bits & (0x80 >> fx)) { - int dx = cx + fx; - if (dx >= pw) break; - pixels[dy * pw + dx] = black_px; - } - } - } - } - } - } + bool cursor_now_visible = t->view_offset == 0 && t->cursor_visible && + t->cursor_x >= 0 && t->cursor_y >= 0 && + t->cursor_x < visible_cols && t->cursor_y < visible_rows; + if (cursor_now_visible) { + TermCell cell = terminal_visible_cell(t, base_row, t->cursor_x, t->cursor_y); + terminal_draw_cursor_pixels(cell, pixels, pw, ph, + t->cursor_x * cell_w, t->cursor_y * cell_h, + cell_w, cell_h, use_ttf, gc); } + + t->render_valid = have_cache; + t->render_cursor_visible = cursor_now_visible; + t->render_cursor_x = t->cursor_x; + t->render_cursor_y = t->cursor_y; + t->render_term_cols = t->cols; + t->render_term_rows = t->rows; + t->render_base_row = base_row; + t->render_pw = pw; + t->render_ph = ph; + t->render_cell_w = cell_w; + t->render_cell_h = cell_h; + t->render_pixels = pixels; + t->dirty = false; } static inline void terminal_resize(TerminalState* t, int new_cols, int new_rows) { @@ -637,6 +807,10 @@ static inline void terminal_resize(TerminalState* t, int new_cols, int new_rows) if (t->cells) montauk::free(t->cells); if (t->alt_cells) montauk::free(t->alt_cells); + if (t->render_cells) { + montauk::free(t->render_cells); + t->render_cells = nullptr; + } t->cells = new_cells; t->alt_cells = new_alt; @@ -645,6 +819,7 @@ static inline void terminal_resize(TerminalState* t, int new_cols, int new_rows) t->scrollback_lines = new_scrollback; t->cursor_x = new_cursor_x; t->cursor_y = new_cursor_y; + terminal_invalidate_render_cache(t); // Clamp view offset if (t->view_offset > t->scrollback_lines) @@ -670,15 +845,28 @@ static inline void terminal_handle_key(TerminalState* t, const Montauk::KeyEvent // Returns false if the child process has exited static inline bool terminal_poll(TerminalState* t) { if (t->child_pid <= 0) return false; - char buf[4096]; - // Drain all available data so large output renders in one frame + char buf[TERM_READ_BUF_SIZE]; + int drained = 0; + int grace_yields = 0; + + // Drain bursty TUI output as one frame. When a child was blocked on a full + // stream, a short yield lets it finish the redraw before we present. for (;;) { int n = montauk::childio_read(t->child_pid, buf, sizeof(buf)); if (n > 0) { terminal_feed(t, buf, n); + drained += n; + grace_yields = 0; + if (drained >= TERM_MAX_DRAIN_BYTES_PER_POLL) + break; } else { // n == -1 means child process is gone; n == 0 means no data yet if (n < 0) { t->child_pid = 0; return false; } + if (drained > 0 && grace_yields < TERM_DRAIN_GRACE_YIELDS) { + grace_yields++; + montauk::yield(); + continue; + } break; } }