From 075c87d52a4d90c0f00f8ba7ba849677771a9be3 Mon Sep 17 00:00:00 2001 From: salmonik Date: Fri, 31 Jul 2026 18:08:45 +0200 Subject: vt: defer scrolls and batch framebuffer pushes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Streaming output scrolled one framebuffer push per line (each a ~3-8 MB write-only copy). The vt now scrolls the logical grid instantly in RAM and defers the display shift: N pending scrolls are pushed together as one shift_up_n — one push regardless of batch size. - vt: PENDING_SCROLLS counter; scroll_up shifts CELLS only; flush renders nothing while pending (the diff would otherwise re-rasterise the whole screen per line); sync_scroll() does one gop::shift_up_n + RENDERED mirror + normal diff flush (which re-blits just the newly exposed rows and the cursor). - Sync triggers: batch of 16 rows, ~40 ms of stream pause (100 Hz TICKS), any CSI sequence (full-screen apps like the editor render immediately), clear_all/set_cursor_visible, interactive input (tty read_line/read_byte), and the shell before each prompt. - gop: shift_up → shift_up_n(n) (one shadow memcpy + one push). Verified in QEMU: mmap/hexdump streaming renders in coherent batches with the prompt syncing at the end; clear wipes clean; the editor scrolls and redraws immediately (45-line file, gutter + status bar intact, cursor correct); save/exit/cat round-trip unchanged. Release and dev builds pass. --- rsnix/src/vt.rs | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 66 insertions(+), 5 deletions(-) (limited to 'rsnix/src/vt.rs') diff --git a/rsnix/src/vt.rs b/rsnix/src/vt.rs index 4db08fc..88129f2 100644 --- a/rsnix/src/vt.rs +++ b/rsnix/src/vt.rs @@ -119,6 +119,7 @@ pub fn size() -> (usize, usize) { } pub fn set_cursor_visible(v: bool) { + sync_scroll(); unsafe { CURSOR_VISIBLE = v; } @@ -261,6 +262,12 @@ fn write_char(c: char) { } fn csi_final(b: u8) { + // A CSI sequence means a full-screen app is actively rendering — + // push any deferred scrolls so it draws over a current framebuffer + // (streaming shell output never emits CSI, so it stays batched). + if unsafe { PENDING_SCROLLS } > 0 { + sync_scroll(); + } match b { // Cursor position (1-based; 0 means 1) b'H' | b'f' => { @@ -400,18 +407,62 @@ fn newline() { } } -/// Shift the whole screen up one row: cell grids in memory, pixels on the -/// framebuffer. Only the exposed bottom row gets re-rasterised by `flush`. +/// Deferred scrolls: the logical grid scrolls instantly (RAM), but the +/// framebuffer shift+push is batched. N pending scrolls cost one push, +/// so a burst of streaming output no longer writes the whole framebuffer +/// once per line. `flush` renders nothing while scrolls are pending; +/// `sync_scroll` pushes the whole batch and lets the normal diff re-blit +/// only the newly-exposed rows. +const SCROLL_BATCH: usize = 16; +static mut PENDING_SCROLLS: usize = 0; +static mut LAST_SYNC_TICK: u16 = 0; + +/// Shift the logical grid up one row; defer the framebuffer shift. fn scroll_up() { let n = unsafe { ROWS * COLS }; let cols = unsafe { COLS }; cells().copy_within(cols..n, 0); - rendered_cells().copy_within(cols..n, 0); for c in 0..cols { cells()[n - cols + c] = BLANK; - rendered_cells()[n - cols + c] = BLANK; } - gop::shift_up(); + unsafe { + PENDING_SCROLLS += 1; + // Batch: sync when enough scrolls piled up, or when the stream + // pauses (~40 ms at the 100 Hz timer) so interactive output still + // appears promptly. + const TICK_LIMIT: u16 = 4; + let now = crate::interrupts::TICKS.load(core::sync::atomic::Ordering::Relaxed); + if PENDING_SCROLLS >= SCROLL_BATCH + || now.wrapping_sub(LAST_SYNC_TICK) > TICK_LIMIT + { + sync_scroll(); + } + } +} + +/// Push deferred scrolls to the display in one batch: shift the shadow +/// (and the rendered grid, which mirrors it) up by the pending count and +/// do a single write-only push, then let the normal flush diff re-blit +/// the newly-exposed rows and redraw the cursor. +pub fn sync_scroll() { + unsafe { + let n = PENDING_SCROLLS; + if n == 0 { + return; + } + let rows = ROWS; + let cols = COLS; + gop::shift_up_n(n); + // Mirror the pushed content in RENDERED (it describes the fb). + let total = rows * cols; + rendered_cells().copy_within(n * cols..total, 0); + for i in (total - n * cols)..total { + rendered_cells()[i] = BLANK; + } + PENDING_SCROLLS = 0; + LAST_SYNC_TICK = crate::interrupts::TICKS.load(core::sync::atomic::Ordering::Relaxed); + flush(); + } } // --------------------------------------------------------------------------- @@ -473,6 +524,7 @@ pub fn clear_all() { unsafe { SGR_FG = 7; SGR_BG = 0; + sync_scroll(); erase_all(); CURSOR_R = 0; CURSOR_C = 0; @@ -490,6 +542,15 @@ pub fn clear_all() { /// (scrolling) or full clears can never leave a stale inverse block that /// the diff can't see (blank-vs-blank). fn flush() { + unsafe { + // Deferred scrolls pending: the framebuffer lags the logical grid. + // Render nothing until `sync_scroll` pushes the batch — the diff + // would otherwise see every cell as changed and re-rasterise the + // whole screen once per line. + if PENDING_SCROLLS > 0 { + return; + } + } let rows = unsafe { ROWS }; let cols = unsafe { COLS }; let n = rows * cols; -- cgit v1.2.3