summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-08-17 20:28:48 +0200
committersalmonik <salmonik@tuta.io>2026-08-17 20:28:48 +0200
commit7bd3459f4f8bae69509202e5aa0dbd04a42c656f (patch)
tree20ce28490635941d3dd88df5732300e3a0689d51
parentTerminal-app design (ADR 0005) + disk-write controller (diff)
downloadrs-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.rs8
-rw-r--r--rsnix/src/gop.rs86
-rw-r--r--rsnix/src/vt.rs1207
-rw-r--r--rsnix/src/wm.rs16
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).
110fn scroll_hook_noop() {}
111
112static mut SCROLL_HOOK: fn() = scroll_hook_noop;
113
114pub fn set_scroll_hook(f: fn()) {
115 unsafe {
116 SCROLL_HOOK = f;
117 }
118}
119
120pub(crate) fn call_scroll_hook() {
121 unsafe {
122 SCROLL_HOOK();
123 }
124}
125
126#[inline] 107#[inline]
127fn stall_display() { 108fn 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.
616pub(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.
666pub(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)]
1228pub(crate) fn shift_up_n(n: usize) { 1276pub(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
19use crate::gop; 13use crate::gop;
20use alloc::boxed::Box;
21use alloc::vec; 14use alloc::vec;
22use alloc::vec::Vec; 15use 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
59static mut CELLS: *mut Vec<Cell> = core::ptr::null_mut();
60/// What is currently on the physical framebuffer (used to diff blits).
61static mut RENDERED: *mut Vec<Cell> = core::ptr::null_mut();
62static mut ROWS: usize = 0;
63static mut COLS: usize = 0;
64
65// ---- cursor ----------------------------------------------------------------
66
67static mut CURSOR_R: usize = 0;
68static mut CURSOR_C: usize = 0;
69static mut LAST_CURSOR: (usize, usize) = (0, 0);
70static mut CURSOR_VISIBLE: bool = true;
71
72// ---- SGR state -------------------------------------------------------------
73
74static mut SGR_FG: u8 = 7;
75static mut SGR_BG: u8 = 0;
76
77// ---- ANSI parser state -----------------------------------------------------
78
79#[derive(Clone, Copy, PartialEq, Eq)] 50#[derive(Clone, Copy, PartialEq, Eq)]
80enum Parser { 51enum Parser {
81 Ground, 52 Ground,
@@ -83,792 +54,662 @@ enum Parser {
83 Csi, 54 Csi,
84} 55}
85 56
86static mut PARSER: Parser = Parser::Ground; 57/// ANSI SGR colour index (0–7) → VGA palette index.
87static mut PRIVATE: bool = false; 58const ANSI_TO_VGA: [u8; 8] = [0, 4, 2, 6, 1, 5, 3, 7];
88static mut PARAMS: [usize; 4] = [0; 4];
89static mut NPARAMS: usize = 0;
90static mut CUR: usize = 0;
91
92// ---------------------------------------------------------------------------
93// init / dimensions
94// ---------------------------------------------------------------------------
95 59
96/// Allocate the cell grid. Call once after `gop::init()`. 60const SCROLL_BATCH: usize = 16;
97pub fn init() { 61const 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 65pub 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
89fn 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
108impl 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 }
141pub 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) {
146pub 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.
153pub 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 }
178pub 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();
202pub 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
355pub 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);
367fn idx(r: usize, c: usize) -> usize { 255 self.flush();
368 r * unsafe { COLS } + c 256 irq_restore(on);
369} 257 }
370
371fn cells() -> &'static mut Vec<Cell> {
372 unsafe { &mut *CELLS }
373}
374
375fn rendered_cells() -> &'static mut Vec<Cell> {
376 unsafe { &mut *RENDERED }
377}
378 258
379fn 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
383fn params() -> &'static mut [usize; 4] { 270 // -- internals ----------------------------------------------------------
384 unsafe { &mut *core::ptr::addr_of_mut!(PARAMS) }
385}
386 271
387fn 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) {
399fn 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
460fn 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;
467fn 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 {
481fn 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
492fn 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.
569const ANSI_TO_VGA: [u8; 8] = [0, 4, 2, 6, 1, 5, 3, 7];
570 427
571fn 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);
598fn 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
606fn 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
614fn 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
620fn 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) {
627fn 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.
644const SCROLL_BATCH: usize = 16;
645static mut PENDING_SCROLLS: usize = 0;
646static 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) {
649fn 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.
673pub 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 605static 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 607fn 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.
704fn 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
721fn 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 { 616pub 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; 638pub fn home_terminal() -> &'static mut Terminal {
731/// undecodable bytes become '?'. 639 home()
732pub fn write_bytes(b: &[u8]) { 640}
733 let on = irq_off(); 641
734 let mut i = 0; 642pub 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 '?'). 652pub 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
656pub fn cell_raw(r: usize, c: usize) -> (u32, u32) {
657 home().cell_raw(r, c)
658}
659
660pub fn set_cursor_visible(v: bool) {
661 home().set_cursor_visible(v);
662}
663
664pub fn sync_scroll() {
665 home().sync_scroll();
666}
667
668pub fn repaint_region(x: usize, y: usize, w: usize, h: usize) {
669 home().repaint_region(x, y, w, h);
670}
671
672pub fn write_bytes(b: &[u8]) {
673 home().write_bytes(b);
674}
675
770pub fn write_str(s: &str) { 676pub 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.
780pub fn put_char(b: u8) { 680pub 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.
788pub fn clear_all() { 684pub 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, 692fn 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).
809fn 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 { 709fn 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.
668pub 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
680fn pump_main() { 664fn pump_main() {
681 loop { 665 loop {
682 if focused().is_some() { 666 if focused().is_some() {