diff options
| author | salmonik <salmonik@tuta.io> | 2026-08-17 20:28:48 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-08-17 20:28:48 +0200 |
| commit | 7bd3459f4f8bae69509202e5aa0dbd04a42c656f (patch) | |
| tree | 20ce28490635941d3dd88df5732300e3a0689d51 | |
| parent | Terminal-app design (ADR 0005) + disk-write controller (diff) | |
| download | rs-dos-7bd3459f4f8bae69509202e5aa0dbd04a42c656f.tar.gz rs-dos-7bd3459f4f8bae69509202e5aa0dbd04a42c656f.zip | |
vt → per-instance Terminal rendering into a pixel buffer
- Refactor vt.rs into a per-instance Terminal struct (owned cell/rendered
grids, cursor, ANSI parser, scroll state), rendering glyphs into a
caller-owned pixel buffer with dirty-region blitting
- Add gop buffer primitives (buffer_glyph_px, buffer_shift_up) and keep
the legacy vt:: API as thin wrappers around the home terminal
- Remove the now-obsolete scroll hook and shadow scroll path
- Home terminal renders into its own buffer; window_id stays None until
the WM integration lands
| -rw-r--r-- | rsdos/src/main.rs | 8 | ||||
| -rw-r--r-- | rsnix/src/gop.rs | 86 | ||||
| -rw-r--r-- | rsnix/src/vt.rs | 1207 | ||||
| -rw-r--r-- | rsnix/src/wm.rs | 16 |
4 files changed, 591 insertions, 726 deletions
diff --git a/rsdos/src/main.rs b/rsdos/src/main.rs index 89df5ea..b147e63 100644 --- a/rsdos/src/main.rs +++ b/rsdos/src/main.rs | |||
| @@ -410,14 +410,6 @@ pub unsafe extern "efiapi" fn efi_main(image_handle: Handle, system_table: *mut | |||
| 410 | // park in poll_key with a fixed frame, so the watchdog's boot-phase spin | 410 | // park in poll_key with a fixed frame, so the watchdog's boot-phase spin |
| 411 | // rule for task 0 is disarmed (boot-phase hangs already panicked). | 411 | // rule for task 0 is disarmed (boot-phase hangs already panicked). |
| 412 | rsnix::sched::mark_shell_running(); | 412 | rsnix::sched::mark_shell_running(); |
| 413 | // Shell-entry self-check: verify the vt grid structs are sane right | ||
| 414 | // before the shell's first print. The OptiPlex crashes in the first | ||
| 415 | // banner write when the cells VecData (or the shadow) holds the | ||
| 416 | // firmware's 0xAA fill instead of real values — this line shows | ||
| 417 | // whether the corruption is already present at shell entry or lands | ||
| 418 | // later, and prints the exact field values when it is. | ||
| 419 | rsnix::vt::debug_verify(); | ||
| 420 | |||
| 421 | // Windowed GUI: initialise the window manager and its input pump. | 413 | // Windowed GUI: initialise the window manager and its input pump. |
| 422 | // The console (vt) stays layer 0; GUI apps are windows composited on top. | 414 | // The console (vt) stays layer 0; GUI apps are windows composited on top. |
| 423 | rsnix::wm::init(); | 415 | rsnix::wm::init(); |
diff --git a/rsnix/src/gop.rs b/rsnix/src/gop.rs index cddad22..61cc78d 100644 --- a/rsnix/src/gop.rs +++ b/rsnix/src/gop.rs | |||
| @@ -104,25 +104,6 @@ pub fn qr_locked() -> bool { | |||
| 104 | QR_LOCKED.load(Ordering::Relaxed) | 104 | QR_LOCKED.load(Ordering::Relaxed) |
| 105 | } | 105 | } |
| 106 | 106 | ||
| 107 | /// Hook invoked after the console scrolls, so the window manager can | ||
| 108 | /// re-composite GUI windows (which sit above the console layer in the | ||
| 109 | /// shadow and must not scroll with it). | ||
| 110 | fn scroll_hook_noop() {} | ||
| 111 | |||
| 112 | static mut SCROLL_HOOK: fn() = scroll_hook_noop; | ||
| 113 | |||
| 114 | pub fn set_scroll_hook(f: fn()) { | ||
| 115 | unsafe { | ||
| 116 | SCROLL_HOOK = f; | ||
| 117 | } | ||
| 118 | } | ||
| 119 | |||
| 120 | pub(crate) fn call_scroll_hook() { | ||
| 121 | unsafe { | ||
| 122 | SCROLL_HOOK(); | ||
| 123 | } | ||
| 124 | } | ||
| 125 | |||
| 126 | #[inline] | 107 | #[inline] |
| 127 | fn stall_display() { | 108 | fn stall_display() { |
| 128 | let n = DISPLAY_STALL.load(Ordering::Relaxed); | 109 | let n = DISPLAY_STALL.load(Ordering::Relaxed); |
| @@ -630,6 +611,71 @@ pub(crate) fn shadow_glyph_px(x: usize, y: usize, ch: u16, fg: (u8, u8, u8), bg: | |||
| 630 | } | 611 | } |
| 631 | } | 612 | } |
| 632 | 613 | ||
| 614 | /// Render one 8×16 glyph into a caller-owned pixel buffer (row-major, `w` | ||
| 615 | /// pixels per row). No push — the caller composites the buffer. | ||
| 616 | pub(crate) fn buffer_glyph_px( | ||
| 617 | buf: *mut u32, | ||
| 618 | w: usize, | ||
| 619 | h: usize, | ||
| 620 | x: usize, | ||
| 621 | y: usize, | ||
| 622 | ch: u16, | ||
| 623 | fg: (u8, u8, u8), | ||
| 624 | bg: (u8, u8, u8), | ||
| 625 | ) { | ||
| 626 | if buf.is_null() { | ||
| 627 | return; | ||
| 628 | } | ||
| 629 | let glyph = font::GLYPH_8X16; | ||
| 630 | let data = if ch < 256 { | ||
| 631 | match glyph.get(ch as usize) { | ||
| 632 | Some(d) => *d, | ||
| 633 | None => return, | ||
| 634 | } | ||
| 635 | } else if let Some(bits) = unicode_glyph_bits(ch) { | ||
| 636 | bits | ||
| 637 | } else { | ||
| 638 | match glyph.get(32) { | ||
| 639 | Some(d) => *d, | ||
| 640 | None => return, | ||
| 641 | } | ||
| 642 | }; | ||
| 643 | let fgc = rgb(fg.0, fg.1, fg.2); | ||
| 644 | let bgc = rgb(bg.0, bg.1, bg.2); | ||
| 645 | unsafe { | ||
| 646 | for cy in 0..16 { | ||
| 647 | let py = y + cy; | ||
| 648 | if py >= h { | ||
| 649 | break; | ||
| 650 | } | ||
| 651 | let row_bits = data[cy]; | ||
| 652 | for cx in 0..8 { | ||
| 653 | let px = x + cx; | ||
| 654 | if px >= w { | ||
| 655 | break; | ||
| 656 | } | ||
| 657 | let on = row_bits & (0x80 >> cx) != 0; | ||
| 658 | *buf.add(py * w + px) = if on { fgc } else { bgc }; | ||
| 659 | } | ||
| 660 | } | ||
| 661 | } | ||
| 662 | } | ||
| 663 | |||
| 664 | /// Shift a caller-owned pixel buffer up by `px` rows, clearing the exposed | ||
| 665 | /// bottom. No push — the caller composites the buffer. | ||
| 666 | pub(crate) fn buffer_shift_up(buf: *mut u32, w: usize, h: usize, px: usize) { | ||
| 667 | if buf.is_null() || px == 0 || px >= h { | ||
| 668 | return; | ||
| 669 | } | ||
| 670 | unsafe { | ||
| 671 | let n = (h - px) * w; | ||
| 672 | core::ptr::copy(buf.add(px * w), buf, n); // memmove (overlap ok) | ||
| 673 | for i in n..(h * w) { | ||
| 674 | *buf.add(i) = 0; | ||
| 675 | } | ||
| 676 | } | ||
| 677 | } | ||
| 678 | |||
| 633 | /// Print crash/fault lines directly to the framebuffer, bypassing the vt | 679 | /// Print crash/fault lines directly to the framebuffer, bypassing the vt |
| 634 | /// entirely (the vt state may be corrupt when an exception fires). | 680 | /// entirely (the vt state may be corrupt when an exception fires). |
| 635 | /// | 681 | /// |
| @@ -958,6 +1004,7 @@ impl Framebuffer { | |||
| 958 | /// With the RAM backbuffer, this is a cached shadow memcpy + a | 1004 | /// With the RAM backbuffer, this is a cached shadow memcpy + a |
| 959 | /// write-only push to the display — the display itself is never read | 1005 | /// write-only push to the display — the display itself is never read |
| 960 | /// (reads are uncached on real hardware and dominate the old path). | 1006 | /// (reads are uncached on real hardware and dominate the old path). |
| 1007 | #[allow(dead_code)] | ||
| 961 | pub(crate) fn shift_up_n(&self, n: usize) { | 1008 | pub(crate) fn shift_up_n(&self, n: usize) { |
| 962 | let n = n.max(1).min(self.height / 16); // sanity | 1009 | let n = n.max(1).min(self.height / 16); // sanity |
| 963 | let row_bytes = self.pitch * 16; | 1010 | let row_bytes = self.pitch * 16; |
| @@ -1225,6 +1272,7 @@ pub(crate) fn blit_glyph(col: usize, row: usize, ch: u16, fg: (u8, u8, u8), bg: | |||
| 1225 | 1272 | ||
| 1226 | /// Shift the framebuffer content up `n` text rows and clear the exposed | 1273 | /// Shift the framebuffer content up `n` text rows and clear the exposed |
| 1227 | /// bottom rows. `n` batches deferred scrolls into one push. | 1274 | /// bottom rows. `n` batches deferred scrolls into one push. |
| 1275 | #[allow(dead_code)] | ||
| 1228 | pub(crate) fn shift_up_n(n: usize) { | 1276 | pub(crate) fn shift_up_n(n: usize) { |
| 1229 | if let Some(fb) = fb().as_ref() { | 1277 | if let Some(fb) = fb().as_ref() { |
| 1230 | fb.shift_up_n(n); | 1278 | fb.shift_up_n(n); |
diff --git a/rsnix/src/vt.rs b/rsnix/src/vt.rs index 4ef568d..90264f6 100644 --- a/rsnix/src/vt.rs +++ b/rsnix/src/vt.rs | |||
| @@ -1,23 +1,16 @@ | |||
| 1 | //! Minimal ANSI terminal emulator over the GOP framebuffer. | 1 | //! Minimal ANSI terminal emulator, now per-instance. |
| 2 | //! | 2 | //! |
| 3 | //! `vt` owns the text screen: a cell grid (char + fg/bg colour), a logical | 3 | //! A `Terminal` owns a cell grid (char + fg/bg colour), a logical cursor, |
| 4 | //! cursor, scrolling, and a small CSI parser. It renders to the framebuffer | 4 | //! scrolling, and a small CSI parser, and renders into a caller-owned pixel |
| 5 | //! through `gop`, blitting only cells whose content changed since the last | 5 | //! buffer (the terminal window's framebuffer). Only cells whose content |
| 6 | //! flush. | 6 | //! changed since the last flush are re-blitted. |
| 7 | //! | 7 | //! |
| 8 | //! This is the kernel's "terminal". Plain text (shell output, typed echo) | 8 | //! This is the kernel's "terminal". Plain text (shell output, typed echo) |
| 9 | //! flows through here in *ground* state; full-screen apps (rsdosedit) write | 9 | //! flows through here in *ground* state; full-screen apps (rsdosedit) write |
| 10 | //! ANSI escape sequences that the CSI parser interprets — cursor addressing, | 10 | //! ANSI escape sequences that the CSI parser interprets — cursor addressing, |
| 11 | //! erase, 16-colour SGR, cursor show/hide. | 11 | //! erase, 16-colour SGR, cursor show/hide. |
| 12 | //! | ||
| 13 | //! Known perf note: scrolling shifts the whole framebuffer one row up (a | ||
| 14 | //! ~3 MB copy at 1024×768), which dominates under QEMU without KVM. A ring | ||
| 15 | //! buffer / deferred-pan approach is the future optimisation; the cell grid | ||
| 16 | //! itself never re-rasterises on scroll — only the exposed bottom row is | ||
| 17 | //! blitted. | ||
| 18 | 12 | ||
| 19 | use crate::gop; | 13 | use crate::gop; |
| 20 | use alloc::boxed::Box; | ||
| 21 | use alloc::vec; | 14 | use alloc::vec; |
| 22 | use alloc::vec::Vec; | 15 | use alloc::vec::Vec; |
| 23 | 16 | ||
| @@ -54,28 +47,6 @@ const BLANK: Cell = Cell { | |||
| 54 | bg: 0, | 47 | bg: 0, |
| 55 | }; | 48 | }; |
| 56 | 49 | ||
| 57 | // ---- screen state ---------------------------------------------------------- | ||
| 58 | |||
| 59 | static mut CELLS: *mut Vec<Cell> = core::ptr::null_mut(); | ||
| 60 | /// What is currently on the physical framebuffer (used to diff blits). | ||
| 61 | static mut RENDERED: *mut Vec<Cell> = core::ptr::null_mut(); | ||
| 62 | static mut ROWS: usize = 0; | ||
| 63 | static mut COLS: usize = 0; | ||
| 64 | |||
| 65 | // ---- cursor ---------------------------------------------------------------- | ||
| 66 | |||
| 67 | static mut CURSOR_R: usize = 0; | ||
| 68 | static mut CURSOR_C: usize = 0; | ||
| 69 | static mut LAST_CURSOR: (usize, usize) = (0, 0); | ||
| 70 | static mut CURSOR_VISIBLE: bool = true; | ||
| 71 | |||
| 72 | // ---- SGR state ------------------------------------------------------------- | ||
| 73 | |||
| 74 | static mut SGR_FG: u8 = 7; | ||
| 75 | static mut SGR_BG: u8 = 0; | ||
| 76 | |||
| 77 | // ---- ANSI parser state ----------------------------------------------------- | ||
| 78 | |||
| 79 | #[derive(Clone, Copy, PartialEq, Eq)] | 50 | #[derive(Clone, Copy, PartialEq, Eq)] |
| 80 | enum Parser { | 51 | enum Parser { |
| 81 | Ground, | 52 | Ground, |
| @@ -83,792 +54,662 @@ enum Parser { | |||
| 83 | Csi, | 54 | Csi, |
| 84 | } | 55 | } |
| 85 | 56 | ||
| 86 | static mut PARSER: Parser = Parser::Ground; | 57 | /// ANSI SGR colour index (0–7) → VGA palette index. |
| 87 | static mut PRIVATE: bool = false; | 58 | const ANSI_TO_VGA: [u8; 8] = [0, 4, 2, 6, 1, 5, 3, 7]; |
| 88 | static mut PARAMS: [usize; 4] = [0; 4]; | ||
| 89 | static mut NPARAMS: usize = 0; | ||
| 90 | static mut CUR: usize = 0; | ||
| 91 | |||
| 92 | // --------------------------------------------------------------------------- | ||
| 93 | // init / dimensions | ||
| 94 | // --------------------------------------------------------------------------- | ||
| 95 | 59 | ||
| 96 | /// Allocate the cell grid. Call once after `gop::init()`. | 60 | const SCROLL_BATCH: usize = 16; |
| 97 | pub fn init() { | 61 | const TICK_LIMIT: u64 = 4; |
| 98 | let (cols, rows) = gop::dims(); | 62 | |
| 99 | if rows == 0 || cols == 0 { | 63 | /// One text terminal: a cell grid + cursor + ANSI parser, rendered into a |
| 100 | // No usable framebuffer geometry: GOP is missing entirely or the | 64 | /// pixel buffer (`w`×`h`, row-major `u32`). |
| 101 | // active mode reports 0x0. Without a display there is nothing | 65 | pub struct Terminal { |
| 102 | // the shell can do, so stop with a visible explanation instead | 66 | cells: Vec<Cell>, |
| 103 | // of building an empty grid and panicking at the first cell | 67 | rendered: Vec<Cell>, |
| 104 | // write (a blank screen on real hardware, which has no serial | 68 | rows: usize, |
| 105 | // console). | 69 | cols: usize, |
| 106 | let lines = [ | 70 | cursor_r: usize, |
| 107 | "FATAL: no framebuffer geometry", | 71 | cursor_c: usize, |
| 108 | "GOP missing or active mode is 0x0", | 72 | last_cursor: (usize, usize), |
| 109 | "see the 'gop:' boot marker / serial log", | 73 | cursor_visible: bool, |
| 110 | ]; | 74 | sgr_fg: u8, |
| 111 | crate::gop::dump_lines(&lines); | 75 | sgr_bg: u8, |
| 112 | crate::serial_println!("FATAL: no framebuffer geometry (GOP missing or 0x0 mode)"); | 76 | parser: Parser, |
| 113 | loop { | 77 | private: bool, |
| 114 | unsafe { | 78 | params: [usize; 4], |
| 115 | core::arch::asm!("cli; hlt"); | 79 | nparams: usize, |
| 116 | } | 80 | cur: usize, |
| 81 | pending_scrolls: usize, | ||
| 82 | last_sync_tick: u64, | ||
| 83 | buffer: Vec<u32>, | ||
| 84 | w: usize, | ||
| 85 | h: usize, | ||
| 86 | window_id: Option<usize>, | ||
| 87 | } | ||
| 88 | |||
| 89 | fn union_rect( | ||
| 90 | d: &mut Option<(usize, usize, usize, usize)>, | ||
| 91 | x: usize, | ||
| 92 | y: usize, | ||
| 93 | w: usize, | ||
| 94 | h: usize, | ||
| 95 | ) { | ||
| 96 | *d = match *d { | ||
| 97 | Some((dx, dy, dw, dh)) => { | ||
| 98 | let nx = dx.min(x); | ||
| 99 | let ny = dy.min(y); | ||
| 100 | let nx2 = (dx + dw).max(x + w); | ||
| 101 | let ny2 = (dy + dh).max(y + h); | ||
| 102 | Some((nx, ny, nx2 - nx, ny2 - ny)) | ||
| 103 | } | ||
| 104 | None => Some((x, y, w, h)), | ||
| 105 | }; | ||
| 106 | } | ||
| 107 | |||
| 108 | impl Terminal { | ||
| 109 | /// Build a terminal with a fresh `w`×`h` pixel buffer. | ||
| 110 | pub fn new(w: usize, h: usize) -> Terminal { | ||
| 111 | let cols = w / 8; | ||
| 112 | let rows = h / 16; | ||
| 113 | Terminal { | ||
| 114 | cells: vec![BLANK; rows * cols], | ||
| 115 | rendered: vec![BLANK; rows * cols], | ||
| 116 | rows, | ||
| 117 | cols, | ||
| 118 | cursor_r: 1, | ||
| 119 | cursor_c: 0, | ||
| 120 | last_cursor: (1, 0), | ||
| 121 | cursor_visible: true, | ||
| 122 | sgr_fg: 7, | ||
| 123 | sgr_bg: 0, | ||
| 124 | parser: Parser::Ground, | ||
| 125 | private: false, | ||
| 126 | params: [0; 4], | ||
| 127 | nparams: 0, | ||
| 128 | cur: 0, | ||
| 129 | pending_scrolls: 0, | ||
| 130 | last_sync_tick: 0, | ||
| 131 | buffer: vec![0; w * h], | ||
| 132 | w, | ||
| 133 | h, | ||
| 134 | window_id: None, | ||
| 117 | } | 135 | } |
| 118 | } | 136 | } |
| 119 | unsafe { | 137 | |
| 120 | ROWS = rows; | 138 | /// Attach this terminal to a WM window (so `flush` composites it). |
| 121 | COLS = cols; | 139 | pub fn set_window(&mut self, id: usize) { |
| 122 | CELLS = Box::into_raw(Box::new(vec![BLANK; rows * cols])); | 140 | self.window_id = Some(id); |
| 123 | RENDERED = Box::into_raw(Box::new(vec![BLANK; rows * cols])); | ||
| 124 | // Home below row 0: row 0 is the boot-marker status line, so the | ||
| 125 | // terminal (shell + klog screen echo) must never write to it. | ||
| 126 | CURSOR_R = 1; | ||
| 127 | CURSOR_C = 0; | ||
| 128 | LAST_CURSOR = (1, 0); | ||
| 129 | CURSOR_VISIBLE = true; | ||
| 130 | SGR_FG = 7; | ||
| 131 | SGR_BG = 0; | ||
| 132 | PARSER = Parser::Ground; | ||
| 133 | NPARAMS = 0; | ||
| 134 | PRIVATE = false; | ||
| 135 | } | 141 | } |
| 136 | } | ||
| 137 | 142 | ||
| 138 | /// True once the terminal is initialised (grids live). The klog screen | 143 | pub fn ready(&self) -> bool { |
| 139 | /// echo checks this so serial lines printed before vt::init (very early | 144 | self.rows != 0 |
| 140 | /// boot) are skipped instead of hitting a null grid. | 145 | } |
| 141 | pub fn ready() -> bool { | ||
| 142 | unsafe { ROWS != 0 && !CELLS.is_null() } | ||
| 143 | } | ||
| 144 | 146 | ||
| 145 | /// (rows, cols) — matches the editor's expected (rows, cols) ordering. | 147 | pub fn size(&self) -> (usize, usize) { |
| 146 | pub fn size() -> (usize, usize) { | 148 | (self.rows, self.cols) |
| 147 | unsafe { (ROWS, COLS) } | 149 | } |
| 148 | } | ||
| 149 | 150 | ||
| 150 | /// Re-render the console's cells into the shadow for a pixel region, so the | 151 | pub fn rows(&self) -> usize { |
| 151 | /// window manager can restore the console (layer 0) beneath a window that | 152 | self.rows |
| 152 | /// moved, closed, or was minimized. Does not push — the caller pushes once. | ||
| 153 | pub fn repaint_region(x: usize, y: usize, w: usize, h: usize) { | ||
| 154 | unsafe { | ||
| 155 | if CELLS.is_null() { | ||
| 156 | return; | ||
| 157 | } | ||
| 158 | let (cols, rows) = (COLS, ROWS); | ||
| 159 | let c0 = x / 8; | ||
| 160 | let c1 = (x + w).div_ceil(8).min(cols); | ||
| 161 | let r0 = y / 16; | ||
| 162 | let r1 = (y + h).div_ceil(16).min(rows); | ||
| 163 | let cells = &*CELLS; | ||
| 164 | for r in r0..r1 { | ||
| 165 | for c in c0..c1 { | ||
| 166 | let cell = cells[r * cols + c]; | ||
| 167 | let fg = PALETTE[cell.fg as usize]; | ||
| 168 | let bg = PALETTE[cell.bg as usize]; | ||
| 169 | crate::gop::shadow_glyph_px(c * 8, r * 16, cell.ch, fg, bg); | ||
| 170 | } | ||
| 171 | } | ||
| 172 | } | 153 | } |
| 173 | } | ||
| 174 | 154 | ||
| 175 | /// DEBUG (crash-family): read one cell's raw word (ch | fg<<16 | bg<<24) | 155 | pub fn cols(&self) -> usize { |
| 176 | /// from the live cells grid and the rendered grid, for the given text | 156 | self.cols |
| 177 | /// row/col. Returns (raw_cell, raw_rendered). | 157 | } |
| 178 | pub fn cell_raw(r: usize, c: usize) -> (u32, u32) { | 158 | |
| 179 | unsafe { | 159 | /// Raw (ch|fg<<16|bg<<24) values of a cell and its rendered mirror, for |
| 180 | if CELLS.is_null() || RENDERED.is_null() { | 160 | /// the `heapdbg` diagnostic. |
| 161 | pub fn cell_raw(&self, r: usize, c: usize) -> (u32, u32) { | ||
| 162 | if r >= self.rows || c >= self.cols { | ||
| 181 | return (0xFFFF_FFFF, 0xFFFF_FFFF); | 163 | return (0xFFFF_FFFF, 0xFFFF_FFFF); |
| 182 | } | 164 | } |
| 183 | let raw = |v: &Vec<Cell>| -> u32 { | 165 | let raw = |cell: &Cell| -> u32 { |
| 184 | if r < ROWS && c < COLS { | 166 | cell.ch as u32 | ((cell.fg as u32) << 16) | ((cell.bg as u32) << 24) |
| 185 | let cell = v[r * COLS + c]; | ||
| 186 | cell.ch as u32 | ((cell.fg as u32) << 16) | ((cell.bg as u32) << 24) | ||
| 187 | } else { | ||
| 188 | 0xFFFF_FFFE | ||
| 189 | } | ||
| 190 | }; | 167 | }; |
| 191 | ( | 168 | let i = self.idx(r, c); |
| 192 | raw(&*(CELLS as *const Vec<Cell>)), | 169 | (raw(&self.cells[i]), raw(&self.rendered[i])) |
| 193 | raw(&*(RENDERED as *const Vec<Cell>)), | ||
| 194 | ) | ||
| 195 | } | 170 | } |
| 196 | } | ||
| 197 | 171 | ||
| 198 | /// Shell-entry self-check: dump the vt grid's heap structs so a real- | 172 | pub fn set_cursor_visible(&mut self, v: bool) { |
| 199 | /// hardware boot shows whether the cells VecData / data arrays are sane | 173 | self.sync_scroll(); |
| 200 | /// at shell entry (the OptiPlex's first banner write #GPs when the | 174 | self.cursor_visible = v; |
| 201 | /// cells buffer holds firmware 0xAA fill instead of real pointers). | 175 | self.flush(); |
| 202 | pub fn debug_verify() { | 176 | } |
| 203 | unsafe { | 177 | |
| 204 | let total = ROWS * COLS; | 178 | /// Push deferred scrolls: shift the pixel buffer, mirror the rendered |
| 205 | let msg = |m: &str| { | 179 | /// grid, and re-blit the exposed rows. |
| 206 | crate::serial_println!("vt: {}", m); | 180 | pub fn sync_scroll(&mut self) { |
| 207 | }; | 181 | let n = self.pending_scrolls; |
| 208 | if CELLS.is_null() || RENDERED.is_null() { | 182 | if n == 0 { |
| 209 | msg("verify: cells/renderd NULL — vt::init did not run"); | ||
| 210 | return; | 183 | return; |
| 211 | } | 184 | } |
| 212 | let c = &*(CELLS as *const Vec<Cell>); | 185 | let rows = self.rows; |
| 213 | let r = &*(RENDERED as *const Vec<Cell>); | 186 | let cols = self.cols; |
| 214 | msg(&alloc::format!( | 187 | gop::buffer_shift_up(self.buffer.as_mut_ptr(), self.w, self.h, n * 16); |
| 215 | "verify: cells ptr={:#x} cap={:#x} len={:#x} rendered ptr={:#x} cap={:#x} len={:#x} want={:#x}", | 188 | // Mirror the pushed content in RENDERED (it describes the buffer). |
| 216 | c.as_ptr() as usize, | 189 | let total = rows * cols; |
| 217 | c.capacity(), | 190 | self.rendered.copy_within(n * cols..total, 0); |
| 218 | c.len(), | 191 | for i in (total - n * cols)..total { |
| 219 | r.as_ptr() as usize, | 192 | self.rendered[i] = BLANK; |
| 220 | r.capacity(), | ||
| 221 | r.len(), | ||
| 222 | total | ||
| 223 | )); | ||
| 224 | // The cells array must cover rows*cols and be inside the heap. | ||
| 225 | let heap_ok = crate::memory::heap_range() | ||
| 226 | .map(|(hs, he)| { | ||
| 227 | let p = c.as_ptr() as usize; | ||
| 228 | let bytes = c.capacity().saturating_mul(core::mem::size_of::<Cell>()); | ||
| 229 | p >= hs && p + bytes <= he | ||
| 230 | }) | ||
| 231 | .unwrap_or(false); | ||
| 232 | // Content check: at shell entry nothing has been printed yet, so | ||
| 233 | // every cell must still hold the grid's init fill. NOTE: that | ||
| 234 | // fill is BLANK (space + fg 7 + bg 0 = raw 0x70020), NOT zero — | ||
| 235 | // `vec![BLANK; n]` fills the grid with BLANK, and all boot | ||
| 236 | // output to this point is serial-only (boot markers are a | ||
| 237 | // direct-FB blit, never the vt grid). The v0.5.7 check | ||
| 238 | // compared against 0 and therefore tripped on every healthy | ||
| 239 | // boot (the "0x70020 at grid(0,0)" mystery — the grid's own | ||
| 240 | // init value, with the probe loop's early `break` leaving the | ||
| 241 | // other three probe slots as zero-initialized). A stale cells | ||
| 242 | // data pointer that happens to be in-heap still passes the | ||
| 243 | // range checks (len/cap look fine) but its "cells" read back as | ||
| 244 | // the underlying memory's leftover fill. NOTE: the OptiPlex | ||
| 245 | // dump value 0x00AAAAAA is the VGA GRAY text colour | ||
| 246 | // (rgb(170,170,170) = PALETTE[7] = fg 7), i.e. rendered shadow | ||
| 247 | // pixels — the memory read as heap held display text, not a | ||
| 248 | // firmware scrub. BLANK (0x70020) vs 0x00AAAAAA is cleanly | ||
| 249 | // distinguishable. | ||
| 250 | let blank_raw = BLANK.ch as u32 | ((BLANK.fg as u32) << 16) | ((BLANK.bg as u32) << 24); | ||
| 251 | let mut content_ok = true; | ||
| 252 | let mut rcontent_ok = true; | ||
| 253 | let mut c_probes_raw: [u32; 4] = [0; 4]; | ||
| 254 | let mut r_probes_raw: [u32; 4] = [0; 4]; | ||
| 255 | if heap_ok && c.len() == total && total > 0 { | ||
| 256 | // Sample the first cell, the last cell, and two interior | ||
| 257 | // cells — reading the whole grid here would be ~60 KiB of | ||
| 258 | // checks at every boot for no real benefit. All four | ||
| 259 | // probes are recorded unconditionally (no early break) so | ||
| 260 | // a serial log shows every value. | ||
| 261 | let probes = [0usize, total / 3, (2 * total) / 3, total - 1]; | ||
| 262 | for (pi, &p) in probes.iter().enumerate() { | ||
| 263 | let cell = *c.as_ptr().add(p); | ||
| 264 | let raw = cell.ch as u32 | ((cell.fg as u32) << 16) | ((cell.bg as u32) << 24); | ||
| 265 | c_probes_raw[pi] = raw; | ||
| 266 | if raw != blank_raw { | ||
| 267 | content_ok = false; | ||
| 268 | } | ||
| 269 | } | ||
| 270 | // Same probe on the rendered grid: it is a *separate* | ||
| 271 | // allocation, so its content discriminates "only the cells | ||
| 272 | // VecData's ptr field is stale" (rendered ok) from "both | ||
| 273 | // grids' memory was clobbered / both VecData ptrs are | ||
| 274 | // stale" (rendered garbage too). | ||
| 275 | for (pi, &p) in probes.iter().enumerate() { | ||
| 276 | let cell = *r.as_ptr().add(p); | ||
| 277 | let raw = cell.ch as u32 | ((cell.fg as u32) << 16) | ((cell.bg as u32) << 24); | ||
| 278 | r_probes_raw[pi] = raw; | ||
| 279 | if raw != blank_raw { | ||
| 280 | rcontent_ok = false; | ||
| 281 | } | ||
| 282 | } | ||
| 283 | } | 193 | } |
| 284 | // Full-grid summary (cheap: a few thousand reads, serial-only). | 194 | self.pending_scrolls = 0; |
| 285 | // Definitive health signal on QEMU: healthy = every cell is | 195 | self.last_sync_tick = crate::interrupts::TICKS.load(core::sync::atomic::Ordering::Relaxed); |
| 286 | // BLANK (nz == blanks == total); the probes above are just the | 196 | self.flush(); |
| 287 | // tripwire, this catches a partial clobber that the four | 197 | } |
| 288 | // samples happen to miss. | 198 | |
| 289 | let (mut c_nz, mut c_blank) = (0usize, 0usize); | 199 | /// Re-blit a region of this terminal's buffer into the shadow (used by |
| 290 | let (mut r_nz, mut r_blank) = (0usize, 0usize); | 200 | /// the window manager to restore the console layer beneath a window). |
| 291 | if heap_ok && c.len() == total && r.len() == total && total > 0 { | 201 | pub fn repaint_region(&self, x: usize, y: usize, w: usize, h: usize) { |
| 292 | for i in 0..total { | 202 | let src = unsafe { self.buffer.as_ptr().add(y * self.w + x) }; |
| 293 | let cell = *c.as_ptr().add(i); | 203 | gop::shadow_blit(x, y, w, h, src, self.w); |
| 294 | let raw = cell.ch as u32 | ((cell.fg as u32) << 16) | ((cell.bg as u32) << 24); | 204 | } |
| 295 | if raw != 0 { | 205 | |
| 296 | c_nz += 1; | 206 | /// Write raw bytes (ANSI-capable), decoding UTF-8 for drawable glyphs. |
| 297 | } | 207 | pub fn write_bytes(&mut self, b: &[u8]) { |
| 298 | if raw == blank_raw { | 208 | let on = irq_off(); |
| 299 | c_blank += 1; | 209 | let mut i = 0; |
| 300 | } | 210 | while i < b.len() { |
| 301 | let cell = *r.as_ptr().add(i); | 211 | let byte = b[i]; |
| 302 | let raw = cell.ch as u32 | ((cell.fg as u32) << 16) | ((cell.bg as u32) << 24); | 212 | if byte < 0x80 { |
| 303 | if raw != 0 { | 213 | self.put_byte(byte); |
| 304 | r_nz += 1; | 214 | i += 1; |
| 215 | continue; | ||
| 216 | } | ||
| 217 | match core::str::from_utf8(&b[i..]) { | ||
| 218 | Ok(s) => { | ||
| 219 | let c = s.chars().next().unwrap_or('?'); | ||
| 220 | self.write_char(c); | ||
| 221 | i += c.len_utf8(); | ||
| 305 | } | 222 | } |
| 306 | if raw == blank_raw { | 223 | Err(e) => { |
| 307 | r_blank += 1; | 224 | let valid = e.valid_up_to(); |
| 225 | if valid > 0 { | ||
| 226 | let s = unsafe { core::str::from_utf8_unchecked(&b[i..i + valid]) }; | ||
| 227 | let c = s.chars().next().unwrap_or('?'); | ||
| 228 | self.write_char(c); | ||
| 229 | i += c.len_utf8(); | ||
| 230 | } else { | ||
| 231 | self.put_byte(b'?'); | ||
| 232 | i += 1; | ||
| 233 | } | ||
| 308 | } | 234 | } |
| 309 | } | 235 | } |
| 310 | } | 236 | } |
| 311 | msg(&alloc::format!( | 237 | self.flush(); |
| 312 | "verify: grids c nz={} blank={} r nz={} blank={} (of {})", | 238 | irq_restore(on); |
| 313 | c_nz, | ||
| 314 | c_blank, | ||
| 315 | r_nz, | ||
| 316 | r_blank, | ||
| 317 | total | ||
| 318 | )); | ||
| 319 | if c.len() != total || r.len() != total || !heap_ok || !content_ok || !rcontent_ok { | ||
| 320 | msg("verify: FAILED — cells/rendered not sane at shell entry"); | ||
| 321 | msg(&alloc::format!( | ||
| 322 | "verify: probes c=[{:#x},{:#x},{:#x},{:#x}] r=[{:#x},{:#x},{:#x},{:#x}]", | ||
| 323 | c_probes_raw[0], | ||
| 324 | c_probes_raw[1], | ||
| 325 | c_probes_raw[2], | ||
| 326 | c_probes_raw[3], | ||
| 327 | r_probes_raw[0], | ||
| 328 | r_probes_raw[1], | ||
| 329 | r_probes_raw[2], | ||
| 330 | r_probes_raw[3] | ||
| 331 | )); | ||
| 332 | // NOTE: debug markers are serial-only now; the crash family | ||
| 333 | // was resolved (EBS-retry free-list rebuild) and the screen | ||
| 334 | // must stay clean for users. | ||
| 335 | crate::serial_println!( | ||
| 336 | "vt: c {:#x} cap={:#x} {} | r {:#x} {}", | ||
| 337 | c.as_ptr() as usize, | ||
| 338 | c.capacity(), | ||
| 339 | if content_ok { "ok" } else { "GARBAGE" }, | ||
| 340 | r.as_ptr() as usize, | ||
| 341 | if rcontent_ok { "ok" } else { "GARBAGE" } | ||
| 342 | ); | ||
| 343 | } else { | ||
| 344 | msg("verify: ok"); | ||
| 345 | crate::serial_println!( | ||
| 346 | "vt: c {:#x} cap={:#x} ok | r {:#x} ok", | ||
| 347 | c.as_ptr() as usize, | ||
| 348 | c.capacity(), | ||
| 349 | r.as_ptr() as usize | ||
| 350 | ); | ||
| 351 | } | ||
| 352 | } | 239 | } |
| 353 | } | ||
| 354 | 240 | ||
| 355 | pub fn set_cursor_visible(v: bool) { | 241 | /// Write a UTF-8 string (plain text; Unicode glyphs render, others '?'). |
| 356 | sync_scroll(); | 242 | pub fn write_str(&mut self, s: &str) { |
| 357 | unsafe { | 243 | let on = irq_off(); |
| 358 | CURSOR_VISIBLE = v; | 244 | for c in s.chars() { |
| 245 | self.write_char(c); | ||
| 246 | } | ||
| 247 | self.flush(); | ||
| 248 | irq_restore(on); | ||
| 359 | } | 249 | } |
| 360 | flush(); | ||
| 361 | } | ||
| 362 | 250 | ||
| 363 | // --------------------------------------------------------------------------- | 251 | /// Write a single ASCII byte / char. |
| 364 | // cell access | 252 | pub fn put_char(&mut self, b: u8) { |
| 365 | // --------------------------------------------------------------------------- | 253 | let on = irq_off(); |
| 366 | 254 | self.put_byte(b); | |
| 367 | fn idx(r: usize, c: usize) -> usize { | 255 | self.flush(); |
| 368 | r * unsafe { COLS } + c | 256 | irq_restore(on); |
| 369 | } | 257 | } |
| 370 | |||
| 371 | fn cells() -> &'static mut Vec<Cell> { | ||
| 372 | unsafe { &mut *CELLS } | ||
| 373 | } | ||
| 374 | |||
| 375 | fn rendered_cells() -> &'static mut Vec<Cell> { | ||
| 376 | unsafe { &mut *RENDERED } | ||
| 377 | } | ||
| 378 | 258 | ||
| 379 | fn cell(r: usize, c: usize) -> &'static mut Cell { | 259 | /// Clear the whole screen, reset colours, home the cursor. |
| 380 | &mut cells()[idx(r, c)] | 260 | pub fn clear_all(&mut self) { |
| 381 | } | 261 | self.sgr_fg = 7; |
| 262 | self.sgr_bg = 0; | ||
| 263 | self.sync_scroll(); | ||
| 264 | self.erase_all(); | ||
| 265 | self.cursor_r = 0; | ||
| 266 | self.cursor_c = 0; | ||
| 267 | self.flush(); | ||
| 268 | } | ||
| 382 | 269 | ||
| 383 | fn params() -> &'static mut [usize; 4] { | 270 | // -- internals ---------------------------------------------------------- |
| 384 | unsafe { &mut *core::ptr::addr_of_mut!(PARAMS) } | ||
| 385 | } | ||
| 386 | 271 | ||
| 387 | fn put_cell(r: usize, c: usize, ch: u16) { | 272 | fn idx(&self, r: usize, c: usize) -> usize { |
| 388 | let cell = cell(r, c); | 273 | r * self.cols + c |
| 389 | cell.ch = ch; | 274 | } |
| 390 | cell.fg = unsafe { SGR_FG }; | ||
| 391 | cell.bg = unsafe { SGR_BG }; | ||
| 392 | } | ||
| 393 | 275 | ||
| 394 | // --------------------------------------------------------------------------- | 276 | fn put_cell(&mut self, r: usize, c: usize, ch: u16) { |
| 395 | // writing — the ANSI state machine | 277 | let i = self.idx(r, c); |
| 396 | // --------------------------------------------------------------------------- | 278 | self.cells[i].ch = ch; |
| 279 | self.cells[i].fg = self.sgr_fg; | ||
| 280 | self.cells[i].bg = self.sgr_bg; | ||
| 281 | } | ||
| 397 | 282 | ||
| 398 | /// Feed one byte through the terminal state machine. | 283 | fn put_byte(&mut self, b: u8) { |
| 399 | fn put_byte(b: u8) { | 284 | match self.parser { |
| 400 | unsafe { | ||
| 401 | match PARSER { | ||
| 402 | Parser::Ground => match b { | 285 | Parser::Ground => match b { |
| 403 | 0x1b => PARSER = Parser::Escape, | 286 | 0x1b => self.parser = Parser::Escape, |
| 404 | 0x0d => CURSOR_C = 0, | 287 | 0x0d => self.cursor_c = 0, |
| 405 | 0x0a => newline(), | 288 | 0x0a => self.newline(), |
| 406 | 0x08 => { | 289 | 0x08 => { |
| 407 | // Backspace: move cursor back and erase. | 290 | if self.cursor_c > 0 { |
| 408 | if CURSOR_C > 0 { | 291 | self.cursor_c -= 1; |
| 409 | CURSOR_C -= 1; | 292 | } else if self.cursor_r > 0 { |
| 410 | } else if CURSOR_R > 0 { | 293 | self.cursor_r -= 1; |
| 411 | CURSOR_R -= 1; | 294 | self.cursor_c = self.cols - 1; |
| 412 | CURSOR_C = COLS - 1; | ||
| 413 | } else { | 295 | } else { |
| 414 | return; | 296 | return; |
| 415 | } | 297 | } |
| 416 | let c = cell(CURSOR_R, CURSOR_C); | 298 | let i = self.idx(self.cursor_r, self.cursor_c); |
| 417 | *c = BLANK; | 299 | self.cells[i] = BLANK; |
| 418 | } | 300 | } |
| 419 | 0x09 => { | 301 | 0x09 => { |
| 420 | let n = 8 - (CURSOR_C % 8); | 302 | let n = 8 - (self.cursor_c % 8); |
| 421 | CURSOR_C = (CURSOR_C + n).min(COLS - 1); | 303 | self.cursor_c = (self.cursor_c + n).min(self.cols - 1); |
| 422 | } | 304 | } |
| 423 | 0x00..=0x1f => {} // other control chars: ignored | 305 | 0x00..=0x1f => {} |
| 424 | _ => put_printable(b as u16), | 306 | _ => self.put_printable(b as u16), |
| 425 | }, | 307 | }, |
| 426 | Parser::Escape => match b { | 308 | Parser::Escape => match b { |
| 427 | b'[' => { | 309 | b'[' => { |
| 428 | PRIVATE = false; | 310 | self.private = false; |
| 429 | NPARAMS = 0; | 311 | self.nparams = 0; |
| 430 | CUR = 0; | 312 | self.cur = 0; |
| 431 | PARSER = Parser::Csi; | 313 | self.parser = Parser::Csi; |
| 432 | } | 314 | } |
| 433 | _ => PARSER = Parser::Ground, // unsupported escape — drop | 315 | _ => self.parser = Parser::Ground, |
| 434 | }, | 316 | }, |
| 435 | Parser::Csi => match b { | 317 | Parser::Csi => match b { |
| 436 | b'?' => PRIVATE = true, | 318 | b'?' => self.private = true, |
| 437 | b'0'..=b'9' => { | 319 | b'0'..=b'9' => { |
| 438 | CUR = CUR.saturating_mul(10).saturating_add((b - b'0') as usize); | 320 | self.cur = self |
| 321 | .cur | ||
| 322 | .saturating_mul(10) | ||
| 323 | .saturating_add((b - b'0') as usize); | ||
| 439 | } | 324 | } |
| 440 | b';' => { | 325 | b';' => { |
| 441 | if NPARAMS < params().len() { | 326 | if self.nparams < self.params.len() { |
| 442 | PARAMS[NPARAMS] = CUR; | 327 | self.params[self.nparams] = self.cur; |
| 443 | NPARAMS += 1; | 328 | self.nparams += 1; |
| 444 | } | 329 | } |
| 445 | CUR = 0; | 330 | self.cur = 0; |
| 446 | } | 331 | } |
| 447 | _ => { | 332 | _ => { |
| 448 | if NPARAMS < params().len() { | 333 | if self.nparams < self.params.len() { |
| 449 | PARAMS[NPARAMS] = CUR; | 334 | self.params[self.nparams] = self.cur; |
| 450 | NPARAMS += 1; | 335 | self.nparams += 1; |
| 451 | } | 336 | } |
| 452 | PARSER = Parser::Ground; | 337 | self.parser = Parser::Ground; |
| 453 | csi_final(b); | 338 | self.csi_final(b); |
| 454 | } | 339 | } |
| 455 | }, | 340 | }, |
| 456 | } | 341 | } |
| 457 | } | 342 | } |
| 458 | } | ||
| 459 | 343 | ||
| 460 | fn param(i: usize, default: usize) -> usize { | 344 | fn param(&self, i: usize, default: usize) -> usize { |
| 461 | unsafe { if i < NPARAMS { PARAMS[i] } else { default } } | 345 | if i < self.nparams { |
| 462 | } | 346 | self.params[i] |
| 347 | } else { | ||
| 348 | default | ||
| 349 | } | ||
| 350 | } | ||
| 463 | 351 | ||
| 464 | /// Place a printable glyph at the cursor and advance (wrapping/scrolling). | 352 | fn put_printable(&mut self, ch: u16) { |
| 465 | /// `ch` is the character code: ASCII or a Unicode codepoint that the | 353 | self.put_cell(self.cursor_r, self.cursor_c, ch); |
| 466 | /// rasteriser can draw (box-drawing / arrows are rendered procedurally). | 354 | self.cursor_c += 1; |
| 467 | fn put_printable(ch: u16) { | 355 | if self.cursor_c >= self.cols { |
| 468 | unsafe { | 356 | self.cursor_c = 0; |
| 469 | put_cell(CURSOR_R, CURSOR_C, ch); | 357 | self.newline(); |
| 470 | CURSOR_C += 1; | ||
| 471 | if CURSOR_C >= COLS { | ||
| 472 | CURSOR_C = 0; | ||
| 473 | newline(); | ||
| 474 | } | 358 | } |
| 475 | } | 359 | } |
| 476 | } | ||
| 477 | 360 | ||
| 478 | /// Dispatch one character to the terminal: ASCII goes through the byte | 361 | fn write_char(&mut self, c: char) { |
| 479 | /// state machine (control/escape handling), printable non-ASCII goes | 362 | let u = c as u32; |
| 480 | /// straight to the glyph path (replaced by '?' when not drawable). | 363 | if u < 0x80 { |
| 481 | fn write_char(c: char) { | 364 | self.put_byte(u as u8); |
| 482 | let u = c as u32; | 365 | } else if (0x2500..=0x257f).contains(&u) || (0x2190..=0x2193).contains(&u) || u == 0x2014 { |
| 483 | if u < 0x80 { | 366 | self.put_printable(c as u16); |
| 484 | put_byte(u as u8); | 367 | } else { |
| 485 | } else if (0x2500..=0x257f).contains(&u) || (0x2190..=0x2193).contains(&u) || u == 0x2014 { | 368 | self.put_byte(b'?'); |
| 486 | put_printable(c as u16); | 369 | } |
| 487 | } else { | ||
| 488 | put_byte(b'?'); | ||
| 489 | } | 370 | } |
| 490 | } | ||
| 491 | 371 | ||
| 492 | fn csi_final(b: u8) { | 372 | fn csi_final(&mut self, b: u8) { |
| 493 | // A CSI sequence means a full-screen app is actively rendering — | 373 | if self.pending_scrolls > 0 { |
| 494 | // push any deferred scrolls so it draws over a current framebuffer | 374 | self.sync_scroll(); |
| 495 | // (streaming shell output never emits CSI, so it stays batched). | ||
| 496 | if unsafe { PENDING_SCROLLS } > 0 { | ||
| 497 | sync_scroll(); | ||
| 498 | } | ||
| 499 | match b { | ||
| 500 | // Cursor position (1-based; 0 means 1) | ||
| 501 | b'H' | b'f' => { | ||
| 502 | let r = param(0, 1).max(1).saturating_sub(1); | ||
| 503 | let c = param(1, 1).max(1).saturating_sub(1); | ||
| 504 | unsafe { | ||
| 505 | CURSOR_R = r.min(ROWS - 1); | ||
| 506 | CURSOR_C = c.min(COLS - 1); | ||
| 507 | } | ||
| 508 | } | 375 | } |
| 509 | // Cursor up / down / forward / back | 376 | match b { |
| 510 | b'A' => unsafe { CURSOR_R = CURSOR_R.saturating_sub(param(0, 1).max(1)) }, | 377 | b'H' | b'f' => { |
| 511 | b'B' => unsafe { CURSOR_R = (CURSOR_R + param(0, 1).max(1)).min(ROWS - 1) }, | 378 | let r = self.param(0, 1).max(1).saturating_sub(1); |
| 512 | b'C' => unsafe { CURSOR_C = (CURSOR_C + param(0, 1).max(1)).min(COLS - 1) }, | 379 | let c = self.param(1, 1).max(1).saturating_sub(1); |
| 513 | b'D' => unsafe { CURSOR_C = CURSOR_C.saturating_sub(param(0, 1).max(1)) }, | 380 | self.cursor_r = r.min(self.rows - 1); |
| 514 | // Erase in display: 0 below, 1 above, 2 all | 381 | self.cursor_c = c.min(self.cols - 1); |
| 515 | b'J' => match param(0, 0) { | 382 | } |
| 516 | 0 => { | 383 | b'A' => self.cursor_r = self.cursor_r.saturating_sub(self.param(0, 1).max(1)), |
| 517 | erase_eol(); | 384 | b'B' => self.cursor_r = (self.cursor_r + self.param(0, 1).max(1)).min(self.rows - 1), |
| 518 | unsafe { | 385 | b'C' => self.cursor_c = (self.cursor_c + self.param(0, 1).max(1)).min(self.cols - 1), |
| 519 | for r in (CURSOR_R + 1)..ROWS { | 386 | b'D' => self.cursor_c = self.cursor_c.saturating_sub(self.param(0, 1).max(1)), |
| 520 | erase_row(r); | 387 | b'J' => match self.param(0, 0) { |
| 388 | 0 => { | ||
| 389 | self.erase_eol(); | ||
| 390 | for r in (self.cursor_r + 1)..self.rows { | ||
| 391 | self.erase_row(r); | ||
| 521 | } | 392 | } |
| 522 | } | 393 | } |
| 523 | } | 394 | 1 => { |
| 524 | 1 => { | 395 | for r in 0..self.cursor_r { |
| 525 | unsafe { | 396 | self.erase_row(r); |
| 526 | for r in 0..CURSOR_R { | ||
| 527 | erase_row(r); | ||
| 528 | } | 397 | } |
| 398 | self.erase_bol(); | ||
| 529 | } | 399 | } |
| 530 | erase_bol(); | 400 | _ => self.erase_all(), |
| 531 | } | 401 | }, |
| 532 | _ => erase_all(), | 402 | b'K' => match self.param(0, 0) { |
| 533 | }, | 403 | 0 => self.erase_eol(), |
| 534 | // Erase in line: 0 to EOL, 1 from BOL, 2 whole line | 404 | 1 => self.erase_bol(), |
| 535 | b'K' => match param(0, 0) { | 405 | _ => self.erase_row(self.cursor_r), |
| 536 | 1 => erase_bol(), | 406 | }, |
| 537 | 2 => { | 407 | b'm' => { |
| 538 | let r = unsafe { CURSOR_R }; | 408 | let n = self.nparams; |
| 539 | erase_row(r); | 409 | if n == 0 { |
| 540 | } | 410 | self.apply_sgr(0); |
| 541 | _ => erase_eol(), | 411 | } else { |
| 542 | }, | 412 | for i in 0..n { |
| 543 | // SGR — colours / attributes | 413 | self.apply_sgr(self.params[i]); |
| 544 | b'm' => { | 414 | } |
| 545 | let n = unsafe { NPARAMS }; | ||
| 546 | if n == 0 { | ||
| 547 | apply_sgr(0); | ||
| 548 | } else { | ||
| 549 | for i in 0..n { | ||
| 550 | apply_sgr(unsafe { PARAMS[i] }); | ||
| 551 | } | 415 | } |
| 552 | } | 416 | } |
| 553 | } | 417 | b'l' | b'h' if self.private => { |
| 554 | // Private mode: 25 = cursor show/hide (h = show, l = hide) | 418 | for i in 0..self.nparams { |
| 555 | b'l' | b'h' if unsafe { PRIVATE } => { | 419 | if self.params[i] == 25 { |
| 556 | for i in 0..unsafe { NPARAMS } { | 420 | self.cursor_visible = b == b'h'; |
| 557 | if unsafe { PARAMS[i] } == 25 { | 421 | } |
| 558 | unsafe { CURSOR_VISIBLE = b == b'h' }; | ||
| 559 | } | 422 | } |
| 560 | } | 423 | } |
| 424 | _ => {} | ||
| 561 | } | 425 | } |
| 562 | _ => {} // unknown / ignored | ||
| 563 | } | 426 | } |
| 564 | } | ||
| 565 | |||
| 566 | /// ANSI SGR colour index (0–7) → VGA palette index. The two orderings | ||
| 567 | /// differ: ANSI 1=red, 3=yellow, 4=blue, 6=cyan; VGA 1=blue, 3=cyan, | ||
| 568 | /// 4=red, 6=brown. Terminals translate ANSI→VGA. | ||
| 569 | const ANSI_TO_VGA: [u8; 8] = [0, 4, 2, 6, 1, 5, 3, 7]; | ||
| 570 | 427 | ||
| 571 | fn apply_sgr(p: usize) { | 428 | fn apply_sgr(&mut self, p: usize) { |
| 572 | unsafe { | ||
| 573 | match p { | 429 | match p { |
| 574 | 0 => { | 430 | 0 => { |
| 575 | SGR_FG = 7; | 431 | self.sgr_fg = 7; |
| 576 | SGR_BG = 0; | 432 | self.sgr_bg = 0; |
| 577 | } | 433 | } |
| 578 | 1 => { | 434 | 1 => { |
| 579 | if SGR_FG < 8 { | 435 | if self.sgr_fg < 8 { |
| 580 | SGR_FG += 8; // bold ≈ bright | 436 | self.sgr_fg += 8; |
| 581 | } | 437 | } |
| 582 | } | 438 | } |
| 583 | 30..=37 => SGR_FG = ANSI_TO_VGA[p - 30], | 439 | 30..=37 => self.sgr_fg = ANSI_TO_VGA[p - 30], |
| 584 | 40..=47 => SGR_BG = ANSI_TO_VGA[p - 40], | 440 | 40..=47 => self.sgr_bg = ANSI_TO_VGA[p - 40], |
| 585 | 90..=97 => SGR_FG = ANSI_TO_VGA[p - 90] + 8, | 441 | 90..=97 => self.sgr_fg = ANSI_TO_VGA[p - 90] + 8, |
| 586 | 100..=107 => SGR_BG = ANSI_TO_VGA[p - 100] + 8, | 442 | 100..=107 => self.sgr_bg = ANSI_TO_VGA[p - 100] + 8, |
| 587 | 39 => SGR_FG = 7, | 443 | 39 => self.sgr_fg = 7, |
| 588 | 49 => SGR_BG = 0, | 444 | 49 => self.sgr_bg = 0, |
| 589 | _ => {} | 445 | _ => {} |
| 590 | } | 446 | } |
| 591 | } | 447 | } |
| 592 | } | ||
| 593 | 448 | ||
| 594 | // --------------------------------------------------------------------------- | 449 | fn erase_eol(&mut self) { |
| 595 | // erase / scroll helpers | 450 | let r = self.cursor_r; |
| 596 | // --------------------------------------------------------------------------- | 451 | for col in self.cursor_c..self.cols { |
| 597 | 452 | let i = self.idx(r, col); | |
| 598 | fn erase_eol() { | 453 | self.cells[i] = BLANK; |
| 599 | let r = unsafe { CURSOR_R }; | 454 | } |
| 600 | let c = unsafe { CURSOR_C }; | ||
| 601 | for col in c..unsafe { COLS } { | ||
| 602 | *cell(r, col) = BLANK; | ||
| 603 | } | 455 | } |
| 604 | } | ||
| 605 | 456 | ||
| 606 | fn erase_bol() { | 457 | fn erase_bol(&mut self) { |
| 607 | let r = unsafe { CURSOR_R }; | 458 | let r = self.cursor_r; |
| 608 | let c = unsafe { CURSOR_C }; | 459 | for col in 0..=self.cursor_c { |
| 609 | for col in 0..=c { | 460 | let i = self.idx(r, col); |
| 610 | *cell(r, col) = BLANK; | 461 | self.cells[i] = BLANK; |
| 462 | } | ||
| 611 | } | 463 | } |
| 612 | } | ||
| 613 | 464 | ||
| 614 | fn erase_row(r: usize) { | 465 | fn erase_row(&mut self, r: usize) { |
| 615 | for col in 0..unsafe { COLS } { | 466 | for col in 0..self.cols { |
| 616 | *cell(r, col) = BLANK; | 467 | let i = self.idx(r, col); |
| 468 | self.cells[i] = BLANK; | ||
| 469 | } | ||
| 617 | } | 470 | } |
| 618 | } | ||
| 619 | 471 | ||
| 620 | fn erase_all() { | 472 | fn erase_all(&mut self) { |
| 621 | for r in 0..unsafe { ROWS } { | 473 | for r in 0..self.rows { |
| 622 | erase_row(r); | 474 | self.erase_row(r); |
| 475 | } | ||
| 623 | } | 476 | } |
| 624 | } | ||
| 625 | 477 | ||
| 626 | /// Advance cursor to the next line (col 0), scrolling if at the bottom. | 478 | fn newline(&mut self) { |
| 627 | fn newline() { | 479 | self.cursor_c = 0; |
| 628 | unsafe { | 480 | if self.cursor_r + 1 < self.rows { |
| 629 | CURSOR_C = 0; | 481 | self.cursor_r += 1; |
| 630 | if CURSOR_R + 1 < ROWS { | ||
| 631 | CURSOR_R += 1; | ||
| 632 | } else { | 482 | } else { |
| 633 | scroll_up(); | 483 | self.scroll_up(); |
| 634 | } | 484 | } |
| 635 | } | 485 | } |
| 636 | } | ||
| 637 | |||
| 638 | /// Deferred scrolls: the logical grid scrolls instantly (RAM), but the | ||
| 639 | /// framebuffer shift+push is batched. N pending scrolls cost one push, | ||
| 640 | /// so a burst of streaming output no longer writes the whole framebuffer | ||
| 641 | /// once per line. `flush` renders nothing while scrolls are pending; | ||
| 642 | /// `sync_scroll` pushes the whole batch and lets the normal diff re-blit | ||
| 643 | /// only the newly-exposed rows. | ||
| 644 | const SCROLL_BATCH: usize = 16; | ||
| 645 | static mut PENDING_SCROLLS: usize = 0; | ||
| 646 | static mut LAST_SYNC_TICK: u64 = 0; | ||
| 647 | 486 | ||
| 648 | /// Shift the logical grid up one row; defer the framebuffer shift. | 487 | fn scroll_up(&mut self) { |
| 649 | fn scroll_up() { | 488 | let n = self.rows * self.cols; |
| 650 | let n = unsafe { ROWS * COLS }; | 489 | let cols = self.cols; |
| 651 | let cols = unsafe { COLS }; | 490 | self.cells.copy_within(cols..n, 0); |
| 652 | cells().copy_within(cols..n, 0); | 491 | for c in 0..cols { |
| 653 | for c in 0..cols { | 492 | self.cells[n - cols + c] = BLANK; |
| 654 | cells()[n - cols + c] = BLANK; | 493 | } |
| 655 | } | 494 | self.pending_scrolls += 1; |
| 656 | unsafe { | ||
| 657 | PENDING_SCROLLS += 1; | ||
| 658 | // Batch: sync when enough scrolls piled up, or when the stream | ||
| 659 | // pauses (~40 ms at the 100 Hz timer) so interactive output still | ||
| 660 | // appears promptly. | ||
| 661 | const TICK_LIMIT: u64 = 4; | ||
| 662 | let now = crate::interrupts::TICKS.load(core::sync::atomic::Ordering::Relaxed); | 495 | let now = crate::interrupts::TICKS.load(core::sync::atomic::Ordering::Relaxed); |
| 663 | if PENDING_SCROLLS >= SCROLL_BATCH || now.wrapping_sub(LAST_SYNC_TICK) > TICK_LIMIT { | 496 | if self.pending_scrolls >= SCROLL_BATCH |
| 664 | sync_scroll(); | 497 | || now.wrapping_sub(self.last_sync_tick) > TICK_LIMIT |
| 498 | { | ||
| 499 | self.sync_scroll(); | ||
| 665 | } | 500 | } |
| 666 | } | 501 | } |
| 667 | } | ||
| 668 | 502 | ||
| 669 | /// Push deferred scrolls to the display in one batch: shift the shadow | 503 | fn flush(&mut self) { |
| 670 | /// (and the rendered grid, which mirrors it) up by the pending count and | 504 | if self.pending_scrolls > 0 { |
| 671 | /// do a single write-only push, then let the normal flush diff re-blit | ||
| 672 | /// the newly-exposed rows and redraw the cursor. | ||
| 673 | pub fn sync_scroll() { | ||
| 674 | unsafe { | ||
| 675 | let n = PENDING_SCROLLS; | ||
| 676 | if n == 0 { | ||
| 677 | return; | 505 | return; |
| 678 | } | 506 | } |
| 679 | let rows = ROWS; | 507 | let rows = self.rows; |
| 680 | let cols = COLS; | 508 | let cols = self.cols; |
| 681 | gop::shift_up_n(n); | 509 | let n = rows * cols; |
| 682 | // Mirror the pushed content in RENDERED (it describes the fb). | 510 | let cursor_visible = self.cursor_visible; |
| 683 | let total = rows * cols; | 511 | let (cr, cc) = (self.cursor_r, self.cursor_c); |
| 684 | rendered_cells().copy_within(n * cols..total, 0); | 512 | let mut dirty: Option<(usize, usize, usize, usize)> = None; |
| 685 | for i in (total - n * cols)..total { | 513 | |
| 686 | rendered_cells()[i] = BLANK; | 514 | // Diff cells vs rendered, re-blitting changed cells into the buffer. |
| 515 | for i in 0..n { | ||
| 516 | let row = i / cols; | ||
| 517 | let col = i % cols; | ||
| 518 | if cursor_visible && row == cr && col == cc { | ||
| 519 | continue; | ||
| 520 | } | ||
| 521 | let c = self.cells[i]; | ||
| 522 | let r = self.rendered[i]; | ||
| 523 | if c != r { | ||
| 524 | let fg = PALETTE[c.fg as usize]; | ||
| 525 | let bg = PALETTE[c.bg as usize]; | ||
| 526 | gop::buffer_glyph_px( | ||
| 527 | self.buffer.as_mut_ptr(), | ||
| 528 | self.w, | ||
| 529 | self.h, | ||
| 530 | col * 8, | ||
| 531 | row * 16, | ||
| 532 | c.ch, | ||
| 533 | fg, | ||
| 534 | bg, | ||
| 535 | ); | ||
| 536 | self.rendered[i] = c; | ||
| 537 | union_rect(&mut dirty, col * 8, row * 16, 8, 16); | ||
| 538 | } | ||
| 539 | } | ||
| 540 | |||
| 541 | // Cursor: erase the previous position, draw the new one (inverse). | ||
| 542 | let (lr, lc) = self.last_cursor; | ||
| 543 | let moved = (lr, lc) != (cr, cc); | ||
| 544 | if (moved || !cursor_visible) && lr < rows && lc < cols { | ||
| 545 | let i = self.idx(lr, lc); | ||
| 546 | let c = self.cells[i]; | ||
| 547 | let fg = PALETTE[c.fg as usize]; | ||
| 548 | let bg = PALETTE[c.bg as usize]; | ||
| 549 | gop::buffer_glyph_px( | ||
| 550 | self.buffer.as_mut_ptr(), | ||
| 551 | self.w, | ||
| 552 | self.h, | ||
| 553 | lc * 8, | ||
| 554 | lr * 16, | ||
| 555 | c.ch, | ||
| 556 | fg, | ||
| 557 | bg, | ||
| 558 | ); | ||
| 559 | self.rendered[i] = c; | ||
| 560 | union_rect(&mut dirty, lc * 8, lr * 16, 8, 16); | ||
| 561 | } | ||
| 562 | if cursor_visible && cr < rows && cc < cols { | ||
| 563 | let i = self.idx(cr, cc); | ||
| 564 | let c = self.cells[i]; | ||
| 565 | let fg = PALETTE[c.bg as usize]; | ||
| 566 | let bg = PALETTE[c.fg as usize]; | ||
| 567 | gop::buffer_glyph_px( | ||
| 568 | self.buffer.as_mut_ptr(), | ||
| 569 | self.w, | ||
| 570 | self.h, | ||
| 571 | cc * 8, | ||
| 572 | cr * 16, | ||
| 573 | c.ch, | ||
| 574 | fg, | ||
| 575 | bg, | ||
| 576 | ); | ||
| 577 | self.rendered[i] = Cell { | ||
| 578 | ch: c.ch, | ||
| 579 | fg: c.bg, | ||
| 580 | bg: c.fg, | ||
| 581 | }; | ||
| 582 | union_rect(&mut dirty, cc * 8, cr * 16, 8, 16); | ||
| 583 | } | ||
| 584 | self.last_cursor = (cr, cc); | ||
| 585 | |||
| 586 | // Display: composite the attached window, or push just the dirty | ||
| 587 | // region of the buffer (not the whole screen). | ||
| 588 | match self.window_id { | ||
| 589 | Some(id) => crate::wm::flush(id), | ||
| 590 | None => { | ||
| 591 | if let Some((x, y, w, h)) = dirty { | ||
| 592 | let src = unsafe { self.buffer.as_ptr().add(y * self.w + x) }; | ||
| 593 | gop::shadow_blit(x, y, w, h, src, self.w); | ||
| 594 | gop::push_rect(x, y, w, h); | ||
| 595 | } | ||
| 596 | } | ||
| 687 | } | 597 | } |
| 688 | PENDING_SCROLLS = 0; | ||
| 689 | LAST_SYNC_TICK = crate::interrupts::TICKS.load(core::sync::atomic::Ordering::Relaxed); | ||
| 690 | flush(); | ||
| 691 | gop::call_scroll_hook(); | ||
| 692 | } | 598 | } |
| 693 | } | 599 | } |
| 694 | 600 | ||
| 695 | // --------------------------------------------------------------------------- | 601 | // --------------------------------------------------------------------------- |
| 696 | // public writing API | 602 | // The home terminal + the legacy `vt::` API (thin wrappers). |
| 697 | // --------------------------------------------------------------------------- | 603 | // --------------------------------------------------------------------------- |
| 698 | 604 | ||
| 699 | /// Disable interrupts unless they are already off (nested-safe). Returns | 605 | static mut HOME: Option<Terminal> = None; |
| 700 | /// whether they were on, so `irq_restore` re-enables exactly when needed. | 606 | |
| 701 | /// The terminal write path uses this so a klog screen-echo (which can run | 607 | fn home() -> &'static mut Terminal { |
| 702 | /// in ISR context) can never tear a shell write, and a task-context write | ||
| 703 | /// can never be split by a tick. | ||
| 704 | fn irq_off() -> bool { | ||
| 705 | let rflags: u64; | ||
| 706 | unsafe { | 608 | unsafe { |
| 707 | core::arch::asm!( | 609 | let slot = &mut *core::ptr::addr_of_mut!(HOME); |
| 708 | "pushfq; pop {}", | 610 | slot.as_mut().expect("vt: not initialised") |
| 709 | out(reg) rflags, | ||
| 710 | options(nostack, preserves_flags) | ||
| 711 | ); | ||
| 712 | if rflags & (1 << 9) != 0 { | ||
| 713 | core::arch::asm!("cli", options(nostack, preserves_flags)); | ||
| 714 | true | ||
| 715 | } else { | ||
| 716 | false | ||
| 717 | } | ||
| 718 | } | 611 | } |
| 719 | } | 612 | } |
| 720 | 613 | ||
| 721 | fn irq_restore(was_on: bool) { | 614 | /// Initialise the home terminal over a full-screen pixel buffer. Call once |
| 722 | if was_on { | 615 | /// after `gop::init()`. |
| 723 | unsafe { | 616 | pub fn init() { |
| 724 | core::arch::asm!("sti", options(nostack, preserves_flags)); | 617 | let (w, h) = gop::pixel_dims(); |
| 618 | if w == 0 || h == 0 { | ||
| 619 | let lines = [ | ||
| 620 | "FATAL: no framebuffer geometry", | ||
| 621 | "GOP missing or active mode is 0x0", | ||
| 622 | "see the 'gop:' boot marker / serial log", | ||
| 623 | ]; | ||
| 624 | crate::gop::dump_lines(&lines); | ||
| 625 | crate::serial_println!("FATAL: no framebuffer geometry (GOP missing or 0x0 mode)"); | ||
| 626 | loop { | ||
| 627 | unsafe { | ||
| 628 | core::arch::asm!("cli; hlt"); | ||
| 629 | } | ||
| 725 | } | 630 | } |
| 726 | } | 631 | } |
| 632 | unsafe { | ||
| 633 | HOME = Some(Terminal::new(w, h)); | ||
| 634 | } | ||
| 727 | } | 635 | } |
| 728 | 636 | ||
| 729 | /// Write raw bytes (ANSI-capable). Used by full-screen apps. UTF-8 | 637 | /// The home terminal instance (for the WM/boot to attach to a window). |
| 730 | /// sequences are decoded so Unicode glyphs (box-drawing, arrows) render; | 638 | pub fn home_terminal() -> &'static mut Terminal { |
| 731 | /// undecodable bytes become '?'. | 639 | home() |
| 732 | pub fn write_bytes(b: &[u8]) { | 640 | } |
| 733 | let on = irq_off(); | 641 | |
| 734 | let mut i = 0; | 642 | pub fn ready() -> bool { |
| 735 | while i < b.len() { | 643 | unsafe { |
| 736 | let byte = b[i]; | 644 | let slot = &*core::ptr::addr_of!(HOME); |
| 737 | if byte < 0x80 { | 645 | match slot { |
| 738 | put_byte(byte); | 646 | Some(t) => t.ready(), |
| 739 | i += 1; | 647 | None => false, |
| 740 | continue; | ||
| 741 | } | ||
| 742 | // Decode the next UTF-8 character starting at i. | ||
| 743 | match core::str::from_utf8(&b[i..]) { | ||
| 744 | Ok(s) => { | ||
| 745 | let c = s.chars().next().unwrap_or('?'); | ||
| 746 | write_char(c); | ||
| 747 | i += c.len_utf8(); | ||
| 748 | } | ||
| 749 | Err(e) => { | ||
| 750 | let valid = e.valid_up_to(); | ||
| 751 | if valid > 0 { | ||
| 752 | let s = unsafe { core::str::from_utf8_unchecked(&b[i..i + valid]) }; | ||
| 753 | let c = s.chars().next().unwrap_or('?'); | ||
| 754 | write_char(c); | ||
| 755 | i += c.len_utf8(); | ||
| 756 | } else { | ||
| 757 | put_byte(b'?'); | ||
| 758 | i += 1; | ||
| 759 | } | ||
| 760 | } | ||
| 761 | } | 648 | } |
| 762 | } | 649 | } |
| 763 | flush(); | ||
| 764 | irq_restore(on); | ||
| 765 | } | 650 | } |
| 766 | 651 | ||
| 767 | /// Write a UTF-8 string (plain text; Unicode glyphs render, others '?'). | 652 | pub fn size() -> (usize, usize) { |
| 768 | /// Atomic with respect to interrupts: a klog screen-echo in ISR context | 653 | home().size() |
| 769 | /// cannot interleave with a shell write, and a tick cannot split a write. | 654 | } |
| 655 | |||
| 656 | pub fn cell_raw(r: usize, c: usize) -> (u32, u32) { | ||
| 657 | home().cell_raw(r, c) | ||
| 658 | } | ||
| 659 | |||
| 660 | pub fn set_cursor_visible(v: bool) { | ||
| 661 | home().set_cursor_visible(v); | ||
| 662 | } | ||
| 663 | |||
| 664 | pub fn sync_scroll() { | ||
| 665 | home().sync_scroll(); | ||
| 666 | } | ||
| 667 | |||
| 668 | pub fn repaint_region(x: usize, y: usize, w: usize, h: usize) { | ||
| 669 | home().repaint_region(x, y, w, h); | ||
| 670 | } | ||
| 671 | |||
| 672 | pub fn write_bytes(b: &[u8]) { | ||
| 673 | home().write_bytes(b); | ||
| 674 | } | ||
| 675 | |||
| 770 | pub fn write_str(s: &str) { | 676 | pub fn write_str(s: &str) { |
| 771 | let on = irq_off(); | 677 | home().write_str(s); |
| 772 | for c in s.chars() { | ||
| 773 | write_char(c); | ||
| 774 | } | ||
| 775 | flush(); | ||
| 776 | irq_restore(on); | ||
| 777 | } | 678 | } |
| 778 | 679 | ||
| 779 | /// Write a single ASCII byte / char. | ||
| 780 | pub fn put_char(b: u8) { | 680 | pub fn put_char(b: u8) { |
| 781 | let on = irq_off(); | 681 | home().put_char(b); |
| 782 | put_byte(b); | ||
| 783 | flush(); | ||
| 784 | irq_restore(on); | ||
| 785 | } | 682 | } |
| 786 | 683 | ||
| 787 | /// Clear the whole screen, reset colours, home the cursor. | ||
| 788 | pub fn clear_all() { | 684 | pub fn clear_all() { |
| 789 | unsafe { | 685 | home().clear_all(); |
| 790 | SGR_FG = 7; | ||
| 791 | SGR_BG = 0; | ||
| 792 | sync_scroll(); | ||
| 793 | erase_all(); | ||
| 794 | CURSOR_R = 0; | ||
| 795 | CURSOR_C = 0; | ||
| 796 | } | ||
| 797 | flush(); | ||
| 798 | } | 686 | } |
| 799 | 687 | ||
| 800 | // --------------------------------------------------------------------------- | 688 | // --------------------------------------------------------------------------- |
| 801 | // rendering | 689 | // Interrupt-flag helpers (atomic terminal writes). |
| 802 | // --------------------------------------------------------------------------- | 690 | // --------------------------------------------------------------------------- |
| 803 | 691 | ||
| 804 | /// Blit every cell whose content differs from what the framebuffer shows, | 692 | fn irq_off() -> bool { |
| 805 | /// then draw the logical cursor (inverse video) on top. The cursor's | 693 | let rflags: u64; |
| 806 | /// inverse colours are mirrored into RENDERED so that pixel shifts | ||
| 807 | /// (scrolling) or full clears can never leave a stale inverse block that | ||
| 808 | /// the diff can't see (blank-vs-blank). | ||
| 809 | fn flush() { | ||
| 810 | unsafe { | 694 | unsafe { |
| 811 | // Deferred scrolls pending: the framebuffer lags the logical grid. | 695 | core::arch::asm!( |
| 812 | // Render nothing until `sync_scroll` pushes the batch — the diff | 696 | "pushfq; pop {}", |
| 813 | // would otherwise see every cell as changed and re-rasterise the | 697 | out(reg) rflags, |
| 814 | // whole screen once per line. | 698 | options(nostack, preserves_flags) |
| 815 | if PENDING_SCROLLS > 0 { | 699 | ); |
| 816 | return; | 700 | if rflags & (1 << 9) != 0 { |
| 817 | } | 701 | core::arch::asm!("cli", options(nostack, preserves_flags)); |
| 818 | } | 702 | true |
| 819 | let rows = unsafe { ROWS }; | 703 | } else { |
| 820 | let cols = unsafe { COLS }; | 704 | false |
| 821 | let n = rows * cols; | ||
| 822 | let cursor_visible = unsafe { CURSOR_VISIBLE }; | ||
| 823 | let (cr, cc) = unsafe { (CURSOR_R, CURSOR_C) }; | ||
| 824 | { | ||
| 825 | let cells = cells(); | ||
| 826 | let rendered = rendered_cells(); | ||
| 827 | for i in 0..n { | ||
| 828 | let row = i / cols; | ||
| 829 | let col = i % cols; | ||
| 830 | // The visible cursor cell is owned by the cursor pass below. | ||
| 831 | if cursor_visible && row == cr && col == cc { | ||
| 832 | continue; | ||
| 833 | } | ||
| 834 | let c = cells[i]; | ||
| 835 | let r = rendered[i]; | ||
| 836 | if c != r { | ||
| 837 | let fg = PALETTE[c.fg as usize]; | ||
| 838 | let bg = PALETTE[c.bg as usize]; | ||
| 839 | gop::blit_glyph(col, row, c.ch, fg, bg); | ||
| 840 | rendered[i] = c; | ||
| 841 | } | ||
| 842 | } | 705 | } |
| 843 | } | 706 | } |
| 707 | } | ||
| 844 | 708 | ||
| 845 | unsafe { | 709 | fn irq_restore(was_on: bool) { |
| 846 | let (lr, lc) = LAST_CURSOR; | 710 | if was_on { |
| 847 | let moved = (lr, lc) != (CURSOR_R, CURSOR_C); | 711 | unsafe { |
| 848 | // Erase the previous cursor position (restore its normal colours) | 712 | core::arch::asm!("sti", options(nostack, preserves_flags)); |
| 849 | // and record it in RENDERED so the diff stays in sync. | ||
| 850 | if (moved || !CURSOR_VISIBLE) && lr < rows && lc < cols { | ||
| 851 | let i = idx(lr, lc); | ||
| 852 | let c = cells()[i]; | ||
| 853 | let fg = PALETTE[c.fg as usize]; | ||
| 854 | let bg = PALETTE[c.bg as usize]; | ||
| 855 | gop::blit_glyph(lc, lr, c.ch, fg, bg); | ||
| 856 | rendered_cells()[i] = c; | ||
| 857 | } | ||
| 858 | LAST_CURSOR = (CURSOR_R, CURSOR_C); | ||
| 859 | // Draw the cursor as an inverse-video cell, mirroring the inverse | ||
| 860 | // colours into RENDERED (scrolling/clearing then repair it). | ||
| 861 | if CURSOR_VISIBLE && CURSOR_R < rows && CURSOR_C < cols { | ||
| 862 | let i = idx(CURSOR_R, CURSOR_C); | ||
| 863 | let c = cells()[i]; | ||
| 864 | let fg = PALETTE[c.fg as usize]; | ||
| 865 | let bg = PALETTE[c.bg as usize]; | ||
| 866 | gop::blit_glyph(CURSOR_C, CURSOR_R, c.ch, bg, fg); | ||
| 867 | rendered_cells()[i] = Cell { | ||
| 868 | ch: c.ch, | ||
| 869 | fg: c.bg, | ||
| 870 | bg: c.fg, | ||
| 871 | }; | ||
| 872 | } | 713 | } |
| 873 | } | 714 | } |
| 874 | } | 715 | } |
diff --git a/rsnix/src/wm.rs b/rsnix/src/wm.rs index 1326696..be74f9a 100644 --- a/rsnix/src/wm.rs +++ b/rsnix/src/wm.rs | |||
| @@ -90,8 +90,6 @@ pub fn init() { | |||
| 90 | CASCADE_Y = 0; | 90 | CASCADE_Y = 0; |
| 91 | } | 91 | } |
| 92 | FOCUSED.store(CONSOLE, Ordering::Relaxed); | 92 | FOCUSED.store(CONSOLE, Ordering::Relaxed); |
| 93 | // When the console scrolls, re-composite windows so they stay put. | ||
| 94 | gop::set_scroll_hook(redraw_windows); | ||
| 95 | } | 93 | } |
| 96 | 94 | ||
| 97 | /// The focused window, or `None` if the console has focus. | 95 | /// The focused window, or `None` if the console has focus. |
| @@ -663,20 +661,6 @@ pub fn spawn_input_pump() -> Option<usize> { | |||
| 663 | crate::sched::spawn(pump_main, "wm-input") | 661 | crate::sched::spawn(pump_main, "wm-input") |
| 664 | } | 662 | } |
| 665 | 663 | ||
| 666 | /// Re-composite every visible window. Called after the console scrolls so | ||
| 667 | /// windows stay in place while the text layer beneath them shifts. | ||
| 668 | pub fn redraw_windows() { | ||
| 669 | unsafe { | ||
| 670 | for i in 0..ZLEN { | ||
| 671 | let id = ZORDER[i]; | ||
| 672 | let w = WINDOWS[id]; | ||
| 673 | if w.used && w.state == WinState::Visible { | ||
| 674 | composite_rect(w.x, w.y, w.w, w.h + TITLE_BAR_H); | ||
| 675 | } | ||
| 676 | } | ||
| 677 | } | ||
| 678 | } | ||
| 679 | |||
| 680 | fn pump_main() { | 664 | fn pump_main() { |
| 681 | loop { | 665 | loop { |
| 682 | if focused().is_some() { | 666 | if focused().is_some() { |
