summaryrefslogtreecommitdiffstats
path: root/rsnix/src/vt.rs
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-07-31 18:08:45 +0200
committersalmonik <salmonik@tuta.io>2026-07-31 18:08:45 +0200
commit075c87d52a4d90c0f00f8ba7ba849677771a9be3 (patch)
tree2d89bc491a3ac1d2d018f1f22c48274e77bd7a08 /rsnix/src/vt.rs
parentgop: RAM backbuffer — scrolling never reads the framebuffer (diff)
downloadrs-dos-075c87d52a4d90c0f00f8ba7ba849677771a9be3.tar.gz
rs-dos-075c87d52a4d90c0f00f8ba7ba849677771a9be3.zip
vt: defer scrolls and batch framebuffer pushes
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.
Diffstat (limited to 'rsnix/src/vt.rs')
-rw-r--r--rsnix/src/vt.rs71
1 files changed, 66 insertions, 5 deletions
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) {
119} 119}
120 120
121pub fn set_cursor_visible(v: bool) { 121pub fn set_cursor_visible(v: bool) {
122 sync_scroll();
122 unsafe { 123 unsafe {
123 CURSOR_VISIBLE = v; 124 CURSOR_VISIBLE = v;
124 } 125 }
@@ -261,6 +262,12 @@ fn write_char(c: char) {
261} 262}
262 263
263fn csi_final(b: u8) { 264fn csi_final(b: u8) {
265 // A CSI sequence means a full-screen app is actively rendering —
266 // push any deferred scrolls so it draws over a current framebuffer
267 // (streaming shell output never emits CSI, so it stays batched).
268 if unsafe { PENDING_SCROLLS } > 0 {
269 sync_scroll();
270 }
264 match b { 271 match b {
265 // Cursor position (1-based; 0 means 1) 272 // Cursor position (1-based; 0 means 1)
266 b'H' | b'f' => { 273 b'H' | b'f' => {
@@ -400,18 +407,62 @@ fn newline() {
400 } 407 }
401} 408}
402 409
403/// Shift the whole screen up one row: cell grids in memory, pixels on the 410/// Deferred scrolls: the logical grid scrolls instantly (RAM), but the
404/// framebuffer. Only the exposed bottom row gets re-rasterised by `flush`. 411/// framebuffer shift+push is batched. N pending scrolls cost one push,
412/// so a burst of streaming output no longer writes the whole framebuffer
413/// once per line. `flush` renders nothing while scrolls are pending;
414/// `sync_scroll` pushes the whole batch and lets the normal diff re-blit
415/// only the newly-exposed rows.
416const SCROLL_BATCH: usize = 16;
417static mut PENDING_SCROLLS: usize = 0;
418static mut LAST_SYNC_TICK: u16 = 0;
419
420/// Shift the logical grid up one row; defer the framebuffer shift.
405fn scroll_up() { 421fn scroll_up() {
406 let n = unsafe { ROWS * COLS }; 422 let n = unsafe { ROWS * COLS };
407 let cols = unsafe { COLS }; 423 let cols = unsafe { COLS };
408 cells().copy_within(cols..n, 0); 424 cells().copy_within(cols..n, 0);
409 rendered_cells().copy_within(cols..n, 0);
410 for c in 0..cols { 425 for c in 0..cols {
411 cells()[n - cols + c] = BLANK; 426 cells()[n - cols + c] = BLANK;
412 rendered_cells()[n - cols + c] = BLANK;
413 } 427 }
414 gop::shift_up(); 428 unsafe {
429 PENDING_SCROLLS += 1;
430 // Batch: sync when enough scrolls piled up, or when the stream
431 // pauses (~40 ms at the 100 Hz timer) so interactive output still
432 // appears promptly.
433 const TICK_LIMIT: u16 = 4;
434 let now = crate::interrupts::TICKS.load(core::sync::atomic::Ordering::Relaxed);
435 if PENDING_SCROLLS >= SCROLL_BATCH
436 || now.wrapping_sub(LAST_SYNC_TICK) > TICK_LIMIT
437 {
438 sync_scroll();
439 }
440 }
441}
442
443/// Push deferred scrolls to the display in one batch: shift the shadow
444/// (and the rendered grid, which mirrors it) up by the pending count and
445/// do a single write-only push, then let the normal flush diff re-blit
446/// the newly-exposed rows and redraw the cursor.
447pub fn sync_scroll() {
448 unsafe {
449 let n = PENDING_SCROLLS;
450 if n == 0 {
451 return;
452 }
453 let rows = ROWS;
454 let cols = COLS;
455 gop::shift_up_n(n);
456 // Mirror the pushed content in RENDERED (it describes the fb).
457 let total = rows * cols;
458 rendered_cells().copy_within(n * cols..total, 0);
459 for i in (total - n * cols)..total {
460 rendered_cells()[i] = BLANK;
461 }
462 PENDING_SCROLLS = 0;
463 LAST_SYNC_TICK = crate::interrupts::TICKS.load(core::sync::atomic::Ordering::Relaxed);
464 flush();
465 }
415} 466}
416 467
417// --------------------------------------------------------------------------- 468// ---------------------------------------------------------------------------
@@ -473,6 +524,7 @@ pub fn clear_all() {
473 unsafe { 524 unsafe {
474 SGR_FG = 7; 525 SGR_FG = 7;
475 SGR_BG = 0; 526 SGR_BG = 0;
527 sync_scroll();
476 erase_all(); 528 erase_all();
477 CURSOR_R = 0; 529 CURSOR_R = 0;
478 CURSOR_C = 0; 530 CURSOR_C = 0;
@@ -490,6 +542,15 @@ pub fn clear_all() {
490/// (scrolling) or full clears can never leave a stale inverse block that 542/// (scrolling) or full clears can never leave a stale inverse block that
491/// the diff can't see (blank-vs-blank). 543/// the diff can't see (blank-vs-blank).
492fn flush() { 544fn flush() {
545 unsafe {
546 // Deferred scrolls pending: the framebuffer lags the logical grid.
547 // Render nothing until `sync_scroll` pushes the batch — the diff
548 // would otherwise see every cell as changed and re-rasterise the
549 // whole screen once per line.
550 if PENDING_SCROLLS > 0 {
551 return;
552 }
553 }
493 let rows = unsafe { ROWS }; 554 let rows = unsafe { ROWS };
494 let cols = unsafe { COLS }; 555 let cols = unsafe { COLS };
495 let n = rows * cols; 556 let n = rows * cols;