summaryrefslogtreecommitdiffstats
path: root/rsnix/src
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
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')
-rw-r--r--rsnix/src/gop.rs31
-rw-r--r--rsnix/src/tty.rs6
-rw-r--r--rsnix/src/vt.rs71
3 files changed, 89 insertions, 19 deletions
diff --git a/rsnix/src/gop.rs b/rsnix/src/gop.rs
index fa5378e..fa14eae 100644
--- a/rsnix/src/gop.rs
+++ b/rsnix/src/gop.rs
@@ -149,14 +149,16 @@ impl Framebuffer {
149 } 149 }
150 } 150 }
151 151
152 /// Shift the whole framebuffer up one row (16 pixels) and clear the 152 /// Shift the framebuffer content up by `n` text rows (16 pixels each)
153 /// exposed bottom row. Called by `vt::scroll_up`; the vt then re-blits 153 /// and clear the exposed bottom rows. Called by `vt` when deferred
154 /// only the new bottom row. 154 /// scrolls are pushed to the display; `n` can batch many scrolls into
155 /// one write-only push.
155 /// 156 ///
156 /// With the RAM backbuffer, this is a cached shadow memcpy + a 157 /// With the RAM backbuffer, this is a cached shadow memcpy + a
157 /// write-only push to the display — the display itself is never read 158 /// write-only push to the display — the display itself is never read
158 /// (reads are uncached on real hardware and dominate the old path). 159 /// (reads are uncached on real hardware and dominate the old path).
159 pub(crate) fn shift_up(&self) { 160 pub(crate) fn shift_up_n(&self, n: usize) {
161 let n = n.max(1).min(self.height / 16); // sanity
160 let row_bytes = self.pitch * 16; 162 let row_bytes = self.pitch * 16;
161 let total = self.pitch * self.height; 163 let total = self.pitch * self.height;
162 let row_words = row_bytes / 4; 164 let row_words = row_bytes / 4;
@@ -165,17 +167,17 @@ impl Framebuffer {
165 if SHADOW.is_null() { 167 if SHADOW.is_null() {
166 // No shadow (allocation failed): fall back to shifting the 168 // No shadow (allocation failed): fall back to shifting the
167 // display directly. 169 // display directly.
168 ptr::copy(self.base.add(row_bytes), self.base, total - row_bytes); 170 ptr::copy(self.base.add(n * row_bytes), self.base, total - n * row_bytes);
169 let last_row = self.base.add(total - row_bytes); 171 let last_row = self.base.add(total - n * row_bytes);
170 for i in 0..row_words { 172 for i in 0..(n * row_words) {
171 ptr::write_volatile(last_row.add(i * 4) as *mut u32, self.bg); 173 ptr::write_volatile(last_row.add(i * 4) as *mut u32, self.bg);
172 } 174 }
173 return; 175 return;
174 } 176 }
175 // Shift the shadow up one row (cached RAM — fast). 177 // Shift the shadow up n rows (cached RAM — fast).
176 ptr::copy(SHADOW.add(row_words), SHADOW, words - row_words); 178 ptr::copy(SHADOW.add(n * row_words), SHADOW, words - n * row_words);
177 // Clear the exposed bottom row in the shadow. 179 // Clear the exposed bottom rows in the shadow.
178 for i in (words - row_words)..words { 180 for i in (words - n * row_words)..words {
179 *SHADOW.add(i) = self.bg; 181 *SHADOW.add(i) = self.bg;
180 } 182 }
181 // Push the shadow to the display: write-only, no reads. 183 // Push the shadow to the display: write-only, no reads.
@@ -372,10 +374,11 @@ pub(crate) fn blit_glyph(col: usize, row: usize, ch: u16, fg: (u8, u8, u8), bg:
372 } 374 }
373} 375}
374 376
375/// Shift the framebuffer up one row and clear the bottom row. 377/// Shift the framebuffer content up `n` text rows and clear the exposed
376pub(crate) fn shift_up() { 378/// bottom rows. `n` batches deferred scrolls into one push.
379pub(crate) fn shift_up_n(n: usize) {
377 if let Some(fb) = unsafe { FB.as_ref() } { 380 if let Some(fb) = unsafe { FB.as_ref() } {
378 fb.shift_up(); 381 fb.shift_up_n(n);
379 } 382 }
380} 383}
381 384
diff --git a/rsnix/src/tty.rs b/rsnix/src/tty.rs
index 8d745b3..b6b6cc8 100644
--- a/rsnix/src/tty.rs
+++ b/rsnix/src/tty.rs
@@ -37,6 +37,9 @@ static mut RAW_LEN: usize = 0;
37/// Read one line in canonical mode: echo every key, ^C cancels the line, 37/// Read one line in canonical mode: echo every key, ^C cancels the line,
38/// ^D on an empty line is EOF. Echo goes to the terminal (GOP) and serial. 38/// ^D on an empty line is EOF. Echo goes to the terminal (GOP) and serial.
39pub fn read_line() -> LineInput { 39pub fn read_line() -> LineInput {
40 // Interactive input: make sure deferred scrolls are pushed first so
41 // the screen shows the current state (no-op when nothing is pending).
42 crate::vt::sync_scroll();
40 let mut buf = strbuf::String::<LINE_BUF_SIZE>::new(); 43 let mut buf = strbuf::String::<LINE_BUF_SIZE>::new();
41 loop { 44 loop {
42 match ps2::poll_key() { 45 match ps2::poll_key() {
@@ -112,6 +115,9 @@ pub fn interrupt_pressed() -> bool {
112/// Blocking read of the next raw-mode byte. Keys arrive translated to the 115/// Blocking read of the next raw-mode byte. Keys arrive translated to the
113/// terminal byte protocol; multi-byte sequences are delivered in order. 116/// terminal byte protocol; multi-byte sequences are delivered in order.
114pub fn read_byte() -> u8 { 117pub fn read_byte() -> u8 {
118 // Interactive (raw) input: push deferred scrolls so the full-screen
119 // app draws over a current framebuffer.
120 crate::vt::sync_scroll();
115 loop { 121 loop {
116 if let Some(b) = pop_raw() { 122 if let Some(b) = pop_raw() {
117 return b; 123 return b;
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;