diff options
| author | salmonik <salmonik@tuta.io> | 2026-07-31 18:08:45 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-07-31 18:08:45 +0200 |
| commit | 075c87d52a4d90c0f00f8ba7ba849677771a9be3 (patch) | |
| tree | 2d89bc491a3ac1d2d018f1f22c48274e77bd7a08 /rsnix/src/vt.rs | |
| parent | gop: RAM backbuffer — scrolling never reads the framebuffer (diff) | |
| download | rs-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 '')
| -rw-r--r-- | rsnix/src/vt.rs | 71 |
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 | ||
| 121 | pub fn set_cursor_visible(v: bool) { | 121 | pub 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 | ||
| 263 | fn csi_final(b: u8) { | 264 | fn 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. | ||
| 416 | const SCROLL_BATCH: usize = 16; | ||
| 417 | static mut PENDING_SCROLLS: usize = 0; | ||
| 418 | static mut LAST_SYNC_TICK: u16 = 0; | ||
| 419 | |||
| 420 | /// Shift the logical grid up one row; defer the framebuffer shift. | ||
| 405 | fn scroll_up() { | 421 | fn 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. | ||
| 447 | pub 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). |
| 492 | fn flush() { | 544 | fn 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; |
