⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

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}