diff options
Diffstat (limited to '')
| -rw-r--r-- | CONTEXT.md | 24 | ||||
| -rw-r--r-- | rsnix/src/gop.rs | 31 | ||||
| -rw-r--r-- | rsnix/src/tty.rs | 6 | ||||
| -rw-r--r-- | rsnix/src/vt.rs | 71 | ||||
| -rw-r--r-- | shell/src/lib.rs | 3 |
5 files changed, 108 insertions, 27 deletions
| @@ -163,14 +163,22 @@ Single Rust crate (`x86_64-unknown-uefi` target). Module breakdown: | |||
| 163 | ## Known issues / perf notes | 163 | ## Known issues / perf notes |
| 164 | 164 | ||
| 165 | - **Scroll cost** — scrolling still shifts the whole framebuffer (a | 165 | - **Scroll cost** — scrolling still shifts the whole framebuffer (a |
| 166 | ~3-8 MB pixel copy per row), but a RAM backbuffer (`gop::SHADOW`) | 166 | ~3-8 MB pixel copy per row), but two mechanisms make it fast on real |
| 167 | makes it fast on real hardware: every raster write goes to the shadow | 167 | hardware: |
| 168 | too, and scrolling is a cached shadow memcpy + one write-only push to | 168 | 1. a RAM backbuffer (`gop::SHADOW`): every raster write goes to the |
| 169 | the display. The display is never *read* — that was the killer, since | 169 | shadow too; scrolling is a cached shadow memcpy + one write-only |
| 170 | real-hardware GOP framebuffers are uncacheable/write-combining and | 170 | push to the display — the display is never *read* (uncached reads |
| 171 | uncached reads cost a bus round-trip per cache line. Still on the | 171 | were the killer, since real-hardware GOP framebuffers are |
| 172 | someday list: a pan-based ring so scrolling never touches the whole | 172 | uncacheable/write-combining). |
| 173 | framebuffer at all. | 173 | 2. deferred scroll batching (`vt::PENDING_SCROLLS`): the logical grid |
| 174 | scrolls instantly in RAM while the framebuffer shift+push is | ||
| 175 | deferred; N pending scrolls cost one push. Bursts of streaming | ||
| 176 | output (hexdump/cat) push every ~16 rows (or after ~40 ms of | ||
| 177 | pause), and the shell syncs before each prompt. Any CSI sequence | ||
| 178 | (full-screen apps like the editor) or interactive input pushes | ||
| 179 | immediately. | ||
| 180 | Still on the someday list: a pan-based ring so scrolling never touches | ||
| 181 | the whole framebuffer at all. | ||
| 174 | - **Editor redraw** re-blits changed cells per keystroke (fine at 1280×800); | 182 | - **Editor redraw** re-blits changed cells per keystroke (fine at 1280×800); |
| 175 | the "optimize drawing someday" item. | 183 | the "optimize drawing someday" item. |
| 176 | - **ramfs only**: edited files live in memory, lost on reboot until disk | 184 | - **ramfs only**: edited files live in memory, lost on reboot until disk |
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 |
| 376 | pub(crate) fn shift_up() { | 378 | /// bottom rows. `n` batches deferred scrolls into one push. |
| 379 | pub(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. |
| 39 | pub fn read_line() -> LineInput { | 39 | pub 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. |
| 114 | pub fn read_byte() -> u8 { | 117 | pub 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 | ||
| 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; |
diff --git a/shell/src/lib.rs b/shell/src/lib.rs index 4601b98..9644f01 100644 --- a/shell/src/lib.rs +++ b/shell/src/lib.rs | |||
| @@ -20,6 +20,9 @@ pub fn run() -> ! { | |||
| 20 | ); | 20 | ); |
| 21 | 21 | ||
| 22 | loop { | 22 | loop { |
| 23 | // Push any deferred scrolls so the prompt renders on a current | ||
| 24 | // framebuffer (e.g. after a streaming command's batch of output). | ||
| 25 | rsnix::vt::sync_scroll(); | ||
| 23 | // Print prompt | 26 | // Print prompt |
| 24 | gop_print!("> "); | 27 | gop_print!("> "); |
| 25 | serial_print!("> "); | 28 | serial_print!("> "); |
