veridian_kernel/graphics/fbcon.rs
1//! Framebuffer console (fbcon) — text rendering onto a pixel framebuffer.
2//!
3//! Renders characters using the 8x16 bitmap font onto the UEFI-provided
4//! (or ramfb-provided) pixel framebuffer. Supports cursor tracking, newlines,
5//! tab stops, backspace, scrolling, and basic ANSI color escape sequences.
6//!
7//! # Performance architecture
8//!
9//! Three-layer pipeline with **eager rendering**, **glyph cache**, and
10//! **incremental dirty-row blitting**:
11//!
12//! 1. **Text cell grid** — character/color pairs in a ring buffer. Scrolling is
13//! O(cols) (advance ring pointer + clear one row of cells).
14//! 2. **RAM back-buffer** — always in sync via eager rendering (each glyph
15//! rendered to pixels immediately on write). Scrolling uses a simple memmove
16//! (~1ms in RAM) and marks all rows dirty (dirty_all). A **glyph cache**
17//! pre-renders all 256 glyphs as u32 pixel arrays for the current color
18//! pair, making glyph rendering a `copy_nonoverlapping` of 32 bytes per row.
19//! 3. **Hardware framebuffer** — MMIO memory. Touched once per `flush()` call,
20//! only for dirty rows — typically one row (~80KB) instead of the full
21//! screen (~4MB). On x86_64, write-combining (PAT) provides faster MMIO
22//! writes.
23//!
24//! Thread-safety: The global `FBCON` is protected by a spinlock. Interrupt
25//! handlers must NOT call `_fbcon_print` (use raw serial output for
26//! diagnostics in ISRs).
27
28use alloc::{vec, vec::Vec};
29use core::{
30 fmt,
31 sync::atomic::{AtomicBool, Ordering},
32};
33
34use spin::Mutex;
35
36use super::font8x16::{self, FONT_HEIGHT, FONT_WIDTH};
37
38/// Controls whether `_fbcon_print` actually renders to the framebuffer.
39/// Starts `false` — boot messages go to serial only (too many lines to
40/// render at 1280x800 in QEMU's emulated CPU). Set to `true` via
41/// `enable_output()` just before the shell launches.
42static FBCON_OUTPUT_ENABLED: AtomicBool = AtomicBool::new(false);
43
44/// Pixel format of the framebuffer.
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum FbPixelFormat {
47 /// Blue-Green-Red-Reserved (UEFI default with OVMF)
48 Bgr,
49 /// Red-Green-Blue-Reserved
50 Rgb,
51}
52
53/// ANSI escape sequence parser state.
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55enum EscapeState {
56 /// Normal text rendering
57 Normal,
58 /// Saw ESC (0x1B), waiting for '['
59 Escape,
60 /// Inside CSI sequence, accumulating parameters
61 Csi,
62}
63
64/// Maximum text rows supported by the console.
65const MAX_ROWS: usize = 64;
66/// Maximum text columns supported by the console.
67const MAX_COLS: usize = 192;
68
69/// Pre-rendered glyph pixel data for the current (fg, bg) color pair.
70///
71/// All 256 glyphs are expanded from 1-bit-per-pixel bitmap to u32-per-pixel
72/// words, so rendering a glyph becomes a `copy_nonoverlapping` of 32 bytes
73/// per row (8 pixels * 4 bytes) instead of 128 per-pixel bit-extractions.
74///
75/// Memory: 256 * 8 * 16 * 4 = 128KB per color pair.
76struct GlyphCache {
77 /// Pre-rendered pixel data: `pixels[glyph * (FONT_WIDTH * FONT_HEIGHT) +
78 /// row * FONT_WIDTH + col]`
79 pixels: Vec<u32>,
80 /// Foreground color word this cache was built for.
81 fg_word: u32,
82 /// Background color word this cache was built for.
83 bg_word: u32,
84}
85
86/// A single character cell in the text grid.
87#[derive(Clone, Copy)]
88struct TextCell {
89 ch: u8,
90 fg: (u8, u8, u8),
91 bg: (u8, u8, u8),
92}
93
94impl TextCell {
95 const fn blank(fg: (u8, u8, u8), bg: (u8, u8, u8)) -> Self {
96 Self { ch: b' ', fg, bg }
97 }
98}
99
100/// Framebuffer console state.
101pub struct FramebufferConsole {
102 // Hardware framebuffer (MMIO, slow in QEMU)
103 fb_ptr: *mut u8,
104 width: usize,
105 height: usize,
106 stride: usize,
107 bpp: usize,
108 pixel_format: FbPixelFormat,
109
110 // Text grid dimensions
111 cols: usize,
112 rows: usize,
113
114 // Cursor
115 cursor_col: usize,
116 cursor_row: usize,
117
118 // Colors
119 fg_color: (u8, u8, u8),
120 bg_color: (u8, u8, u8),
121 default_fg: (u8, u8, u8),
122 default_bg: (u8, u8, u8),
123
124 // ANSI escape parser
125 esc_state: EscapeState,
126 esc_params: [u8; 16],
127 esc_param_idx: usize,
128
129 // Text cell ring buffer (Phase 2)
130 cells: Vec<TextCell>,
131 ring_start: usize,
132
133 // RAM back-buffer (Phase 1) — `None` if allocation failed (fallback to direct MMIO)
134 back_buf: Option<Vec<u8>>,
135
136 // Glyph cache — pre-rendered pixel data for the current (fg, bg) pair.
137 // `None` if allocation failed (OOM fallback to per-pixel path).
138 glyph_cache: Option<GlyphCache>,
139
140 // Cursor overlay (drawn on MMIO only, never touches back-buffer)
141 cursor_visible: bool,
142 display_cursor_col: usize,
143 display_cursor_row: usize,
144
145 // Dirty tracking (Phase 1+2)
146 dirty_rows: [bool; MAX_ROWS],
147 dirty_all: bool,
148}
149
150// SAFETY: FramebufferConsole is only accessed through the FBCON spinlock.
151// The raw pointer fb_ptr points to MMIO memory that is valid for the
152// kernel's lifetime (mapped by the bootloader or ramfb init).
153unsafe impl Send for FramebufferConsole {}
154
155/// Default foreground: light gray
156const DEFAULT_FG: (u8, u8, u8) = (0xAA, 0xAA, 0xAA);
157/// Default background: black
158const DEFAULT_BG: (u8, u8, u8) = (0x00, 0x00, 0x00);
159
160/// ANSI standard colors (SGR 30-37 foreground, 40-47 background)
161const ANSI_COLORS: [(u8, u8, u8); 8] = [
162 (0x00, 0x00, 0x00), // 0: black
163 (0xAA, 0x00, 0x00), // 1: red
164 (0x00, 0xAA, 0x00), // 2: green
165 (0xAA, 0x55, 0x00), // 3: yellow/brown
166 (0x00, 0x00, 0xAA), // 4: blue
167 (0xAA, 0x00, 0xAA), // 5: magenta
168 (0x00, 0xAA, 0xAA), // 6: cyan
169 (0xAA, 0xAA, 0xAA), // 7: white/light gray
170];
171
172static FBCON: Mutex<Option<FramebufferConsole>> = Mutex::new(None);
173
174impl FramebufferConsole {
175 /// Create a new framebuffer console.
176 fn new(
177 fb_ptr: *mut u8,
178 width: usize,
179 height: usize,
180 stride: usize,
181 bpp: usize,
182 pixel_format: FbPixelFormat,
183 ) -> Self {
184 let cols = (width / FONT_WIDTH).min(MAX_COLS);
185 let rows = (height / FONT_HEIGHT).min(MAX_ROWS);
186
187 // Allocate text cell grid
188 let cell_count = MAX_ROWS * MAX_COLS;
189 let cells = vec![TextCell::blank(DEFAULT_FG, DEFAULT_BG); cell_count];
190
191 // Allocate RAM back-buffer. If heap is exhausted, fall back to
192 // direct MMIO rendering (same as old behavior).
193 let buf_size = stride * height;
194 let back_buf = try_alloc_vec(buf_size).ok();
195
196 // Allocate glyph cache (128KB). OOM is non-fatal — falls back to
197 // per-pixel rendering.
198 let glyph_cache = try_alloc_glyph_cache();
199
200 Self {
201 fb_ptr,
202 width,
203 height,
204 stride,
205 bpp,
206 pixel_format,
207 cols,
208 rows,
209 cursor_col: 0,
210 cursor_row: 0,
211 fg_color: DEFAULT_FG,
212 bg_color: DEFAULT_BG,
213 default_fg: DEFAULT_FG,
214 default_bg: DEFAULT_BG,
215 esc_state: EscapeState::Normal,
216 esc_params: [0; 16],
217 esc_param_idx: 0,
218 cells,
219 ring_start: 0,
220 back_buf,
221 glyph_cache,
222 cursor_visible: false,
223 display_cursor_col: 0,
224 display_cursor_row: 0,
225 dirty_rows: [false; MAX_ROWS],
226 dirty_all: true, // Force initial full blit
227 }
228 }
229
230 /// Zero the hardware framebuffer and back-buffer on initial setup.
231 /// This clears any UEFI boot garbage from the screen.
232 fn clear_hw_and_backbuf(&mut self) {
233 let total_bytes = self.stride * self.height;
234 // SAFETY: fb_ptr is valid for total_bytes (caller guarantees).
235 unsafe {
236 core::ptr::write_bytes(self.fb_ptr, 0, total_bytes);
237 }
238 if let Some(ref mut buf) = self.back_buf {
239 // SAFETY: buf is exactly total_bytes in size.
240 unsafe {
241 core::ptr::write_bytes(buf.as_mut_ptr(), 0, buf.len());
242 }
243 }
244 self.dirty_all = false; // Screen is already clear
245 }
246
247 /// Map a logical text row (0 = top of screen) to the physical ring index.
248 #[inline(always)]
249 fn phys_row(&self, logical_row: usize) -> usize {
250 (self.ring_start + logical_row) % MAX_ROWS
251 }
252
253 /// Get a reference to a text cell.
254 #[inline(always)]
255 fn cell(&self, logical_row: usize, col: usize) -> &TextCell {
256 let idx = self.phys_row(logical_row) * MAX_COLS + col;
257 &self.cells[idx]
258 }
259
260 /// Compute the flat index for a text cell.
261 #[inline(always)]
262 fn cell_idx(&self, logical_row: usize, col: usize) -> usize {
263 self.phys_row(logical_row) * MAX_COLS + col
264 }
265
266 /// Mark a logical text row as dirty (needs re-rendering on next blit).
267 #[inline(always)]
268 fn mark_dirty(&mut self, logical_row: usize) {
269 if logical_row < MAX_ROWS {
270 self.dirty_rows[logical_row] = true;
271 }
272 }
273
274 /// Convert an RGB triple to a u32 pixel word in the framebuffer's format.
275 #[inline(always)]
276 fn color_to_word(&self, r: u8, g: u8, b: u8) -> u32 {
277 match self.pixel_format {
278 FbPixelFormat::Bgr => u32::from_ne_bytes([b, g, r, 0]),
279 FbPixelFormat::Rgb => u32::from_ne_bytes([r, g, b, 0]),
280 }
281 }
282
283 /// Rebuild the glyph cache for a new (fg, bg) color pair.
284 ///
285 /// Expands all 256 glyphs from 1-bit-per-pixel bitmaps into u32 pixel
286 /// words. Called on first render and on every SGR color change (rare).
287 fn rebuild_glyph_cache(&mut self, fg_word: u32, bg_word: u32) {
288 let cache = match self.glyph_cache {
289 Some(ref mut c) => c,
290 None => return,
291 };
292 cache.fg_word = fg_word;
293 cache.bg_word = bg_word;
294
295 for glyph in 0..256u16 {
296 let glyph_data = font8x16::glyph(glyph as u8);
297 let base = glyph as usize * FONT_WIDTH * FONT_HEIGHT;
298 for (row, &bits) in glyph_data.iter().enumerate() {
299 for col in 0..FONT_WIDTH {
300 let word = if (bits >> (7 - col)) & 1 != 0 {
301 fg_word
302 } else {
303 bg_word
304 };
305 cache.pixels[base + row * FONT_WIDTH + col] = word;
306 }
307 }
308 }
309 }
310
311 /// Render a single glyph to the back-buffer (or directly to HW FB if no
312 /// back-buffer is available). Writes 8x16 = 128 pixels.
313 ///
314 /// Optimization layers:
315 /// 1. **Glyph cache hit**: `copy_nonoverlapping` of 32 bytes per row (16
316 /// rows = 512 bytes total) from pre-rendered cache.
317 /// 2. **Cache rebuild**: If colors changed, rebuild all 256 glyphs (~300us)
318 /// then cache hit.
319 /// 3. **Fallback**: Per-pixel bit extraction (no cache available).
320 fn render_glyph_to_buf(
321 &mut self,
322 ch: u8,
323 px: usize,
324 py: usize,
325 fg: (u8, u8, u8),
326 bg: (u8, u8, u8),
327 ) {
328 let fg_word = self.color_to_word(fg.0, fg.1, fg.2);
329 let bg_word = self.color_to_word(bg.0, bg.1, bg.2);
330 let stride = self.stride;
331 let bpp = self.bpp;
332
333 let buf_ptr = match self.back_buf {
334 Some(ref mut buf) => buf.as_mut_ptr(),
335 None => self.fb_ptr,
336 };
337
338 let base_offset = py * stride;
339
340 // Try glyph cache path
341 if self.glyph_cache.is_some() {
342 // Check if cache colors match; rebuild if needed
343 {
344 let needs_rebuild = match self.glyph_cache {
345 Some(ref c) => c.fg_word != fg_word || c.bg_word != bg_word,
346 None => false,
347 };
348 if needs_rebuild {
349 self.rebuild_glyph_cache(fg_word, bg_word);
350 }
351 }
352
353 if let Some(ref cache) = self.glyph_cache {
354 let glyph_base = ch as usize * FONT_WIDTH * FONT_HEIGHT;
355 // SAFETY: buf_ptr is valid for stride * height bytes (back-buffer)
356 // or the HW FB. base_offset is within bounds. Each row copies
357 // FONT_WIDTH u32 words (32 bytes) which is within bounds.
358 unsafe {
359 for row in 0..FONT_HEIGHT {
360 let buf_offset = base_offset + row * stride + px * bpp;
361 let dst = buf_ptr.add(buf_offset) as *mut u32;
362 let src = cache.pixels.as_ptr().add(glyph_base + row * FONT_WIDTH);
363 core::ptr::copy_nonoverlapping(src, dst, FONT_WIDTH);
364 }
365 }
366 return;
367 }
368 }
369
370 // Fallback: per-pixel bit extraction (no glyph cache)
371 let glyph_data = font8x16::glyph(ch);
372 for (row, &bits) in glyph_data.iter().enumerate() {
373 let buf_offset = base_offset + row * stride + px * bpp;
374 // SAFETY: Same bounds guarantee as the cached path.
375 unsafe {
376 let ptr = buf_ptr.add(buf_offset) as *mut u32;
377 for col in 0..FONT_WIDTH {
378 let word = if (bits >> (7 - col)) & 1 != 0 {
379 fg_word
380 } else {
381 bg_word
382 };
383 ptr.add(col).write(word);
384 }
385 }
386 }
387 }
388
389 /// Render one logical text row from the cell grid to the back-buffer.
390 ///
391 /// No longer called in the normal rendering path (eager rendering +
392 /// pixel ring scroll keep the back-buffer up-to-date). Retained for
393 /// diagnostic/debug use.
394 #[allow(dead_code)] // Diagnostic/debug utility
395 fn render_row_to_backbuf(&mut self, logical_row: usize) {
396 let py = logical_row * FONT_HEIGHT;
397
398 // Check if the row is all blank cells with black bg (common case for
399 // empty rows after scroll).
400 let mut all_blank_black = true;
401 for col in 0..self.cols {
402 let c = self.cell(logical_row, col);
403 if c.ch != b' ' || c.bg != (0, 0, 0) {
404 all_blank_black = false;
405 break;
406 }
407 }
408
409 if all_blank_black {
410 // Fast path: zero the pixel region with memset
411 let base_offset = py * self.stride;
412 let row_bytes = FONT_HEIGHT * self.stride;
413 let buf_ptr = match self.back_buf {
414 Some(ref mut buf) => buf.as_mut_ptr(),
415 None => self.fb_ptr,
416 };
417 // SAFETY: base_offset + row_bytes is within the buffer bounds.
418 unsafe {
419 core::ptr::write_bytes(buf_ptr.add(base_offset), 0, row_bytes);
420 }
421 return;
422 }
423
424 // Check if the row is all blank cells with uniform non-black bg
425 let first_bg = self.cell(logical_row, 0).bg;
426 let mut all_blank_uniform = true;
427 for col in 0..self.cols {
428 let c = self.cell(logical_row, col);
429 if c.ch != b' ' || c.bg != first_bg {
430 all_blank_uniform = false;
431 break;
432 }
433 }
434
435 if all_blank_uniform {
436 // Fill with uniform bg color
437 let bg_word = self.color_to_word(first_bg.0, first_bg.1, first_bg.2);
438 let buf_ptr = match self.back_buf {
439 Some(ref mut buf) => buf.as_mut_ptr(),
440 None => self.fb_ptr,
441 };
442 let stride = self.stride;
443 let base_offset = py * stride;
444 for row in 0..FONT_HEIGHT {
445 // SAFETY: writing within buffer bounds.
446 unsafe {
447 let base = base_offset + row * stride;
448 let ptr = buf_ptr.add(base) as *mut u32;
449 for x in 0..self.width {
450 ptr.add(x).write(bg_word);
451 }
452 }
453 }
454 return;
455 }
456
457 // General case: render each cell's glyph
458 for col in 0..self.cols {
459 let c = *self.cell(logical_row, col);
460 self.render_glyph_to_buf(c.ch, col * FONT_WIDTH, py, c.fg, c.bg);
461 }
462 }
463
464 /// Draw a block cursor on the MMIO framebuffer (inverse video).
465 ///
466 /// Reads the glyph at the current cursor position and renders it with
467 /// fg/bg swapped directly on MMIO. The back-buffer is never touched,
468 /// so blits automatically erase the cursor.
469 fn draw_cursor(&mut self) {
470 if self.back_buf.is_none() {
471 return;
472 }
473 let row = self.cursor_row;
474 let col = self.cursor_col;
475 if row >= self.rows || col >= self.cols {
476 return;
477 }
478
479 let cell = *self.cell(row, col);
480 // Swap fg/bg for inverse video. For blank cells on black bg, use a
481 // visible gray block so the cursor is always visible.
482 let (fg, bg) = if cell.ch == b' ' && cell.bg == (0, 0, 0) {
483 ((0, 0, 0), (0xAA, 0xAA, 0xAA))
484 } else {
485 (cell.bg, cell.fg)
486 };
487
488 let fg_word = self.color_to_word(fg.0, fg.1, fg.2);
489 let bg_word = self.color_to_word(bg.0, bg.1, bg.2);
490 let ch = if cell.ch == b' ' { b' ' } else { cell.ch };
491 let px = col * FONT_WIDTH;
492 let py = row * FONT_HEIGHT;
493 let stride = self.stride;
494 let bpp = self.bpp;
495
496 // Render glyph with swapped colors directly to MMIO
497 let glyph_data = font8x16::glyph(ch);
498 for (glyph_row, &bits) in glyph_data.iter().enumerate() {
499 let offset = (py + glyph_row) * stride + px * bpp;
500 // SAFETY: fb_ptr is valid for stride * height bytes. The cursor
501 // position is within visible bounds (checked above).
502 unsafe {
503 let ptr = self.fb_ptr.add(offset) as *mut u32;
504 for glyph_col in 0..FONT_WIDTH {
505 let word = if (bits >> (7 - glyph_col)) & 1 != 0 {
506 fg_word
507 } else {
508 bg_word
509 };
510 ptr.add(glyph_col).write(word);
511 }
512 }
513 }
514
515 self.cursor_visible = true;
516 self.display_cursor_col = col;
517 self.display_cursor_row = row;
518 }
519
520 /// Erase the cursor overlay by copying the original pixels from the
521 /// back-buffer to MMIO at the cursor's displayed position.
522 fn erase_cursor(&mut self) {
523 if !self.cursor_visible {
524 return;
525 }
526 let row = self.display_cursor_row;
527 let col = self.display_cursor_col;
528 if row >= self.rows || col >= self.cols {
529 self.cursor_visible = false;
530 return;
531 }
532
533 if let Some(ref buf) = self.back_buf {
534 let px = col * FONT_WIDTH;
535 let py = row * FONT_HEIGHT;
536 let stride = self.stride;
537 let bpp = self.bpp;
538
539 for glyph_row in 0..FONT_HEIGHT {
540 let offset = (py + glyph_row) * stride + px * bpp;
541 let bytes = FONT_WIDTH * bpp;
542 // SAFETY: back-buffer and fb_ptr are both valid for stride * height
543 // bytes. The cursor position is within visible bounds.
544 unsafe {
545 core::ptr::copy_nonoverlapping(
546 buf.as_ptr().add(offset),
547 self.fb_ptr.add(offset),
548 bytes,
549 );
550 }
551 }
552 }
553 self.cursor_visible = false;
554 }
555
556 /// Blit dirty regions from back-buffer to the hardware framebuffer.
557 ///
558 /// Back-buffer is always up-to-date from eager rendering — this is a
559 /// pure memcpy with zero font lookups or per-pixel branching.
560 ///
561 /// The back-buffer uses linear layout (no ring). `dirty_all` copies the
562 /// entire visible area; otherwise only individually dirty rows are blitted.
563 /// The cursor overlay is erased before blitting and redrawn after.
564 fn blit_to_framebuffer(&mut self) {
565 let back_buf = match self.back_buf {
566 Some(ref buf) => buf.as_ptr(),
567 None => return, // No back-buffer — eager rendering went directly to HW FB
568 };
569
570 // Erase cursor before blitting so the blit overwrites the cursor area
571 // with clean back-buffer pixels. Cursor is redrawn after.
572 self.erase_cursor();
573
574 let text_row_bytes = FONT_HEIGHT * self.stride;
575 let pixel_buf_size = self.rows * text_row_bytes;
576
577 if self.dirty_all {
578 // Straight copy — back-buffer is linear, same layout as MMIO
579 // SAFETY: back_buf is `stride * height` bytes (>= pixel_buf_size).
580 // fb_ptr is at least pixel_buf_size bytes.
581 unsafe {
582 core::ptr::copy_nonoverlapping(back_buf, self.fb_ptr, pixel_buf_size);
583 }
584 self.dirty_all = false;
585 for flag in self.dirty_rows[..self.rows].iter_mut() {
586 *flag = false;
587 }
588 } else {
589 // Per dirty row: same offset in back-buffer and MMIO
590 for row in 0..self.rows {
591 if self.dirty_rows[row] {
592 let offset = row * text_row_bytes;
593 // SAFETY: offset + text_row_bytes <= pixel_buf_size.
594 unsafe {
595 core::ptr::copy_nonoverlapping(
596 back_buf.add(offset),
597 self.fb_ptr.add(offset),
598 text_row_bytes,
599 );
600 }
601 self.dirty_rows[row] = false;
602 }
603 }
604 }
605
606 // Redraw cursor on MMIO after blit
607 self.draw_cursor();
608 }
609
610 /// Scroll the text grid up by one row.
611 ///
612 /// Cell ring: O(cols) (advance pointer + clear one row).
613 /// Back-buffer: memmove all rows up by one text row (~3MB, ~1ms in RAM),
614 /// then clear the new bottom row. All rows are marked dirty (dirty_all)
615 /// since the memmove shifts every row's MMIO position.
616 /// Fallback (no back-buffer): memmove on MMIO (slow but correct).
617 fn scroll_up(&mut self) {
618 // The row that was at the top is recycled to become the new bottom
619 let old_top_phys = self.ring_start;
620 self.ring_start = (self.ring_start + 1) % MAX_ROWS;
621
622 // Clear the recycled row (now the last visible row)
623 let new_bottom_phys = old_top_phys;
624 let base = new_bottom_phys * MAX_COLS;
625 for col in 0..self.cols {
626 self.cells[base + col] = TextCell::blank(self.fg_color, self.bg_color);
627 }
628
629 let text_row_bytes = FONT_HEIGHT * self.stride;
630 let visible_bytes = self.rows * text_row_bytes;
631
632 if let Some(ref mut buf) = self.back_buf {
633 // Shift all rows up by one text row in RAM (memmove, ~3MB, ~1ms)
634 // SAFETY: buf is stride * height bytes (>= visible_bytes).
635 // Source and destination overlap, so we use copy (memmove).
636 unsafe {
637 core::ptr::copy(
638 buf.as_ptr().add(text_row_bytes),
639 buf.as_mut_ptr(),
640 visible_bytes - text_row_bytes,
641 );
642 // Clear the new bottom row
643 core::ptr::write_bytes(
644 buf.as_mut_ptr().add(visible_bytes - text_row_bytes),
645 0,
646 text_row_bytes,
647 );
648 }
649 // After memmove, every MMIO row is stale — mark all dirty.
650 // This is safe: per-keystroke uses flush_row() (ignores dirty_all),
651 // and flush() with dirty_all runs only at command boundaries (~20ms).
652 self.dirty_all = true;
653 } else {
654 // No back-buffer: memmove on MMIO (fallback, slow but correct)
655 // SAFETY: fb_ptr valid for stride * height bytes. Source and dest
656 // overlap so we use copy (memmove semantics).
657 unsafe {
658 core::ptr::copy(
659 self.fb_ptr.add(text_row_bytes),
660 self.fb_ptr,
661 visible_bytes - text_row_bytes,
662 );
663 core::ptr::write_bytes(
664 self.fb_ptr.add(visible_bytes - text_row_bytes),
665 0,
666 text_row_bytes,
667 );
668 }
669 }
670 }
671
672 /// Write a character at the current cursor position and advance.
673 fn write_char(&mut self, ch: u8) {
674 match self.esc_state {
675 EscapeState::Normal => self.write_char_normal(ch),
676 EscapeState::Escape => self.write_char_escape(ch),
677 EscapeState::Csi => self.write_char_csi(ch),
678 }
679 }
680
681 /// Handle a character in normal (non-escape) mode.
682 fn write_char_normal(&mut self, ch: u8) {
683 match ch {
684 b'\n' => {
685 self.cursor_col = 0;
686 self.cursor_row += 1;
687 if self.cursor_row >= self.rows {
688 self.scroll_up();
689 self.cursor_row = self.rows - 1;
690 }
691 }
692 b'\r' => {
693 self.cursor_col = 0;
694 }
695 b'\t' => {
696 let next_tab = (self.cursor_col + 8) & !7;
697 self.cursor_col = if next_tab < self.cols {
698 next_tab
699 } else {
700 self.cols - 1
701 };
702 }
703 0x08 => {
704 // Backspace
705 if self.cursor_col > 0 {
706 self.cursor_col -= 1;
707 let fg = self.fg_color;
708 let bg = self.bg_color;
709 let idx = self.cell_idx(self.cursor_row, self.cursor_col);
710 self.cells[idx] = TextCell { ch: b' ', fg, bg };
711 // Eagerly render blank glyph to back-buffer
712 let px = self.cursor_col * FONT_WIDTH;
713 let py = self.cursor_row * FONT_HEIGHT;
714 self.render_glyph_to_buf(b' ', px, py, fg, bg);
715 self.mark_dirty(self.cursor_row);
716 }
717 }
718 0x1B => {
719 // ESC — start escape sequence
720 self.esc_state = EscapeState::Escape;
721 self.esc_param_idx = 0;
722 self.esc_params = [0; 16];
723 }
724 _ => {
725 // Printable character — update text cell
726 let fg = self.fg_color;
727 let bg = self.bg_color;
728 let idx = self.cell_idx(self.cursor_row, self.cursor_col);
729 self.cells[idx] = TextCell { ch, fg, bg };
730 // Eagerly render glyph to back-buffer (1 glyph = 128 pixel writes to RAM)
731 let px = self.cursor_col * FONT_WIDTH;
732 let py = self.cursor_row * FONT_HEIGHT;
733 self.render_glyph_to_buf(ch, px, py, fg, bg);
734 self.mark_dirty(self.cursor_row);
735
736 self.cursor_col += 1;
737 if self.cursor_col >= self.cols {
738 self.cursor_col = 0;
739 self.cursor_row += 1;
740 if self.cursor_row >= self.rows {
741 self.scroll_up();
742 self.cursor_row = self.rows - 1;
743 }
744 }
745 }
746 }
747 }
748
749 /// Handle a character after ESC was received.
750 fn write_char_escape(&mut self, ch: u8) {
751 if ch == b'[' {
752 self.esc_state = EscapeState::Csi;
753 } else {
754 // Unknown escape sequence — discard and return to normal
755 self.esc_state = EscapeState::Normal;
756 }
757 }
758
759 /// Handle a character inside a CSI sequence (ESC [ ...).
760 fn write_char_csi(&mut self, ch: u8) {
761 match ch {
762 b'0'..=b'9' => {
763 // Accumulate parameter digit
764 if self.esc_param_idx < self.esc_params.len() {
765 self.esc_params[self.esc_param_idx] = self.esc_params[self.esc_param_idx]
766 .wrapping_mul(10)
767 .wrapping_add(ch - b'0');
768 }
769 }
770 b';' => {
771 // Parameter separator
772 if self.esc_param_idx < self.esc_params.len() - 1 {
773 self.esc_param_idx += 1;
774 }
775 }
776 b'm' => {
777 // SGR (Select Graphic Rendition)
778 self.handle_sgr();
779 self.esc_state = EscapeState::Normal;
780 }
781 b'J' => {
782 // Erase in Display
783 let param = self.esc_params[0];
784 if param == 2 {
785 self.clear();
786 }
787 self.esc_state = EscapeState::Normal;
788 }
789 b'H' => {
790 // Cursor Position
791 let row = if self.esc_params[0] > 0 {
792 (self.esc_params[0] - 1) as usize
793 } else {
794 0
795 };
796 let col = if self.esc_param_idx >= 1 && self.esc_params[1] > 0 {
797 (self.esc_params[1] - 1) as usize
798 } else {
799 0
800 };
801 self.cursor_row = if row < self.rows { row } else { self.rows - 1 };
802 self.cursor_col = if col < self.cols { col } else { self.cols - 1 };
803 self.esc_state = EscapeState::Normal;
804 }
805 b'A' => {
806 // Cursor Up
807 let n = if self.esc_params[0] > 0 {
808 self.esc_params[0] as usize
809 } else {
810 1
811 };
812 self.cursor_row = self.cursor_row.saturating_sub(n);
813 self.esc_state = EscapeState::Normal;
814 }
815 b'B' => {
816 // Cursor Down
817 let n = if self.esc_params[0] > 0 {
818 self.esc_params[0] as usize
819 } else {
820 1
821 };
822 self.cursor_row = core::cmp::min(self.cursor_row + n, self.rows - 1);
823 self.esc_state = EscapeState::Normal;
824 }
825 b'C' => {
826 // Cursor Forward
827 let n = if self.esc_params[0] > 0 {
828 self.esc_params[0] as usize
829 } else {
830 1
831 };
832 self.cursor_col = core::cmp::min(self.cursor_col + n, self.cols - 1);
833 self.esc_state = EscapeState::Normal;
834 }
835 b'D' => {
836 // Cursor Back
837 let n = if self.esc_params[0] > 0 {
838 self.esc_params[0] as usize
839 } else {
840 1
841 };
842 self.cursor_col = self.cursor_col.saturating_sub(n);
843 self.esc_state = EscapeState::Normal;
844 }
845 b'K' => {
846 // Erase in Line (param 0 = cursor to end, 1 = start to cursor, 2 = whole line)
847 let param = self.esc_params[0];
848 let (start_col, end_col) = match param {
849 1 => (0, self.cursor_col),
850 2 => (0, self.cols),
851 _ => (self.cursor_col, self.cols), // 0 or default
852 };
853 let fg = self.fg_color;
854 let bg = self.bg_color;
855 for col in start_col..end_col {
856 let idx = self.cell_idx(self.cursor_row, col);
857 self.cells[idx] = TextCell { ch: b' ', fg, bg };
858 }
859 // Eagerly render erased region to back-buffer
860 let py = self.cursor_row * FONT_HEIGHT;
861 let end = end_col.min(self.cols);
862 if bg == (0, 0, 0) {
863 // Fast path: zero pixel region
864 let px_start = start_col * FONT_WIDTH * self.bpp;
865 let px_width = (end - start_col) * FONT_WIDTH * self.bpp;
866 let base_offset = py * self.stride;
867 let buf_ptr = match self.back_buf {
868 Some(ref mut buf) => buf.as_mut_ptr(),
869 None => self.fb_ptr,
870 };
871 for row in 0..FONT_HEIGHT {
872 let offset = base_offset + row * self.stride + px_start;
873 // SAFETY: offset + px_width is within the buffer bounds.
874 unsafe {
875 core::ptr::write_bytes(buf_ptr.add(offset), 0, px_width);
876 }
877 }
878 } else {
879 for col in start_col..end {
880 self.render_glyph_to_buf(b' ', col * FONT_WIDTH, py, fg, bg);
881 }
882 }
883 self.mark_dirty(self.cursor_row);
884 self.esc_state = EscapeState::Normal;
885 }
886 _ => {
887 // Unknown CSI command — discard sequence
888 self.esc_state = EscapeState::Normal;
889 }
890 }
891 }
892
893 /// Handle SGR (Select Graphic Rendition) escape codes.
894 fn handle_sgr(&mut self) {
895 let param_count = self.esc_param_idx + 1;
896 for i in 0..param_count {
897 let code = self.esc_params[i];
898 match code {
899 0 => {
900 // Reset
901 self.fg_color = self.default_fg;
902 self.bg_color = self.default_bg;
903 }
904 1 => {
905 // Bold — use bright variants (add 0x55 to each channel, cap at 0xFF)
906 self.fg_color = (
907 self.fg_color.0.saturating_add(0x55),
908 self.fg_color.1.saturating_add(0x55),
909 self.fg_color.2.saturating_add(0x55),
910 );
911 }
912 30..=37 => {
913 self.fg_color = ANSI_COLORS[(code - 30) as usize];
914 }
915 40..=47 => {
916 self.bg_color = ANSI_COLORS[(code - 40) as usize];
917 }
918 _ => {} // Ignore unsupported SGR codes
919 }
920 }
921 }
922
923 /// Clear the entire screen — reset text cells, cursor, and pixel ring.
924 fn clear(&mut self) {
925 let fg = self.fg_color;
926 let bg = self.bg_color;
927 let blank = TextCell { ch: b' ', fg, bg };
928 for row in 0..self.rows {
929 for col in 0..self.cols {
930 let idx = self.cell_idx(row, col);
931 self.cells[idx] = blank;
932 }
933 }
934 // Eagerly clear back-buffer pixels
935 let total_bytes = self.stride * self.height;
936 if bg == (0, 0, 0) {
937 if let Some(ref mut buf) = self.back_buf {
938 // SAFETY: buf is exactly total_bytes in size.
939 unsafe {
940 core::ptr::write_bytes(buf.as_mut_ptr(), 0, total_bytes);
941 }
942 } else {
943 // SAFETY: fb_ptr is valid for total_bytes.
944 unsafe {
945 core::ptr::write_bytes(self.fb_ptr, 0, total_bytes);
946 }
947 }
948 } else {
949 let bg_word = self.color_to_word(bg.0, bg.1, bg.2);
950 let buf_ptr = match self.back_buf {
951 Some(ref mut buf) => buf.as_mut_ptr(),
952 None => self.fb_ptr,
953 };
954 // SAFETY: total_bytes is stride * height, writing u32 words.
955 unsafe {
956 let ptr = buf_ptr as *mut u32;
957 for i in 0..(total_bytes / 4) {
958 ptr.add(i).write(bg_word);
959 }
960 }
961 }
962 self.cursor_col = 0;
963 self.cursor_row = 0;
964 self.dirty_all = true;
965 }
966}
967
968impl fmt::Write for FramebufferConsole {
969 fn write_str(&mut self, s: &str) -> fmt::Result {
970 for byte in s.bytes() {
971 self.write_char(byte);
972 }
973 Ok(())
974 }
975}
976
977/// Initialize the framebuffer console with the given parameters.
978///
979/// Must be called after the framebuffer is available (after UEFI boot
980/// on x86_64, or after ramfb init on AArch64/RISC-V).
981///
982/// # Safety
983///
984/// `fb_ptr` must point to a valid framebuffer of at least `stride * height`
985/// bytes, mapped for the kernel's lifetime.
986pub unsafe fn init(
987 fb_ptr: *mut u8,
988 width: usize,
989 height: usize,
990 stride: usize,
991 bpp: usize,
992 format: FbPixelFormat,
993) {
994 let mut fbcon = FramebufferConsole::new(fb_ptr, width, height, stride, bpp, format);
995 // Log back-buffer and glyph cache allocation status to serial.
996 // If the back-buffer failed, all rendering goes directly to MMIO (very slow).
997 crate::serial::_serial_print(format_args!(
998 "[FBCON] {}x{} stride={} bpp={} back_buf={} glyph_cache={}\n",
999 width,
1000 height,
1001 stride,
1002 bpp,
1003 if fbcon.back_buf.is_some() {
1004 "OK"
1005 } else {
1006 "FAILED (direct MMIO)"
1007 },
1008 if fbcon.glyph_cache.is_some() {
1009 "OK"
1010 } else {
1011 "FAILED"
1012 },
1013 ));
1014 fbcon.clear_hw_and_backbuf();
1015 *FBCON.lock() = Some(fbcon);
1016}
1017
1018/// Enable fbcon output. Called after boot completes (just before the
1019/// shell launches) so that the hundreds of boot log lines don't get
1020/// rendered pixel-by-pixel to the framebuffer in QEMU.
1021pub fn enable_output() {
1022 FBCON_OUTPUT_ENABLED.store(true, Ordering::Release);
1023}
1024
1025/// Print formatted text to the framebuffer console.
1026///
1027/// Silently returns if fbcon has not been initialized yet or if output
1028/// has not been enabled (boot messages go to serial only for performance).
1029///
1030/// Text is written to the cell grid (fast RAM) only. Call [`flush()`] to
1031/// blit pending changes to the hardware framebuffer. This decoupling
1032/// allows multi-line output (e.g. `help`) to accumulate in RAM and then
1033/// hit the slow MMIO path only once.
1034pub fn _fbcon_print(args: fmt::Arguments) {
1035 if !FBCON_OUTPUT_ENABLED.load(Ordering::Relaxed) {
1036 return;
1037 }
1038 use fmt::Write;
1039 let mut guard = FBCON.lock();
1040 if let Some(ref mut fbcon) = *guard {
1041 let _ = fbcon.write_fmt(args);
1042 }
1043}
1044
1045/// Blit all pending changes to the hardware framebuffer.
1046///
1047/// Call this after a logical group of output is complete (e.g., after a
1048/// command finishes, after printing the prompt, after echoing a keystroke).
1049/// This is the ONLY code path that writes to MMIO.
1050pub fn flush() {
1051 if !FBCON_OUTPUT_ENABLED.load(Ordering::Relaxed) {
1052 return;
1053 }
1054 let mut guard = FBCON.lock();
1055 if let Some(ref mut fbcon) = *guard {
1056 fbcon.blit_to_framebuffer();
1057 }
1058}
1059
1060/// Blit a single text row from back-buffer to MMIO and clear its dirty flag.
1061///
1062/// Use this for targeted updates (e.g., after echoing a keystroke) instead of
1063/// `flush()`, which scans all rows.
1064pub fn flush_row(logical_row: usize) {
1065 if !FBCON_OUTPUT_ENABLED.load(Ordering::Relaxed) {
1066 return;
1067 }
1068 let mut guard = FBCON.lock();
1069 if let Some(ref mut fbcon) = *guard {
1070 if logical_row >= fbcon.rows {
1071 return;
1072 }
1073 if let Some(ref buf) = fbcon.back_buf {
1074 let text_row_bytes = FONT_HEIGHT * fbcon.stride;
1075 let offset = logical_row * text_row_bytes;
1076 // SAFETY: offset + text_row_bytes is within the back-buffer
1077 // (which is stride * height bytes) and within the MMIO FB.
1078 unsafe {
1079 core::ptr::copy_nonoverlapping(
1080 buf.as_ptr().add(offset),
1081 fbcon.fb_ptr.add(offset),
1082 text_row_bytes,
1083 );
1084 }
1085 fbcon.dirty_rows[logical_row] = false;
1086 }
1087 }
1088}
1089
1090/// Return the current text cursor row (for targeted row flushing).
1091pub fn cursor_row() -> usize {
1092 let guard = FBCON.lock();
1093 guard.as_ref().map(|f| f.cursor_row).unwrap_or(0)
1094}
1095
1096/// Update the cursor overlay on the MMIO framebuffer.
1097///
1098/// Erases the old cursor position and draws a new block cursor (inverse
1099/// video) at the current text cursor position. Only touches MMIO — the
1100/// back-buffer is never modified.
1101pub fn update_cursor() {
1102 if !FBCON_OUTPUT_ENABLED.load(Ordering::Relaxed) {
1103 return;
1104 }
1105 let mut guard = FBCON.lock();
1106 if let Some(ref mut fbcon) = *guard {
1107 fbcon.erase_cursor();
1108 fbcon.draw_cursor();
1109 }
1110}
1111
1112/// Check if the framebuffer console has been initialized.
1113pub fn is_initialized() -> bool {
1114 FBCON.lock().is_some()
1115}
1116
1117/// Framebuffer hardware information for the compositor.
1118pub struct FbHwInfo {
1119 pub fb_ptr: *mut u8,
1120 pub width: usize,
1121 pub height: usize,
1122 pub stride: usize,
1123 pub bpp: usize,
1124 pub pixel_format: FbPixelFormat,
1125}
1126
1127// SAFETY: FbHwInfo contains a raw pointer to MMIO memory that is valid
1128// for the kernel's lifetime. The pointer is only used for direct pixel
1129// writes by the compositor, serialized by the caller.
1130unsafe impl Send for FbHwInfo {}
1131
1132/// Get the hardware framebuffer information.
1133///
1134/// Returns None if fbcon has not been initialized.
1135pub fn get_hw_info() -> Option<FbHwInfo> {
1136 let guard = FBCON.lock();
1137 guard.as_ref().map(|fbcon| FbHwInfo {
1138 fb_ptr: fbcon.fb_ptr,
1139 width: fbcon.width,
1140 height: fbcon.height,
1141 stride: fbcon.stride,
1142 bpp: fbcon.bpp,
1143 pixel_format: fbcon.pixel_format,
1144 })
1145}
1146
1147/// Disable fbcon text output (when switching to GUI compositor).
1148pub fn disable_output() {
1149 FBCON_OUTPUT_ENABLED.store(false, Ordering::Release);
1150}
1151
1152/// Mark all rows dirty and trigger a full blit on the next flush.
1153///
1154/// Used when returning from GUI mode to repaint the entire text console.
1155pub fn mark_all_dirty_and_flush() {
1156 FBCON_OUTPUT_ENABLED.store(true, Ordering::Release);
1157 let mut guard = FBCON.lock();
1158 if let Some(ref mut fbcon) = *guard {
1159 fbcon.dirty_all = true;
1160 fbcon.blit_to_framebuffer();
1161 }
1162}
1163
1164/// Try to allocate a Vec<u8> of the given size, zeroed.
1165/// Returns Err if allocation fails (OOM).
1166fn try_alloc_vec(size: usize) -> Result<Vec<u8>, ()> {
1167 let mut v = Vec::new();
1168 if v.try_reserve_exact(size).is_err() {
1169 return Err(());
1170 }
1171 v.resize(size, 0);
1172 Ok(v)
1173}
1174
1175/// Try to allocate a glyph cache (128KB). Returns `None` on OOM.
1176fn try_alloc_glyph_cache() -> Option<GlyphCache> {
1177 let count = 256 * FONT_WIDTH * FONT_HEIGHT; // 32,768 u32 entries = 128KB
1178 let mut pixels = Vec::new();
1179 if pixels.try_reserve_exact(count).is_err() {
1180 return None;
1181 }
1182 pixels.resize(count, 0u32);
1183 Some(GlyphCache {
1184 pixels,
1185 fg_word: 0,
1186 bg_word: 0,
1187 })
1188}