Skip to main content

strat9_kernel/arch/x86_64/vga/
writer.rs

1use super::{
2    api::current_ui_scale,
3    cursor::{CursorManager, CURSOR_H, CURSOR_PIXELS, CURSOR_W, TEXT_CURSOR_MAX_DIM},
4    font::{parse_psf, parse_psf2_unicode_map, FontInfo, FONT_PSF},
5    scrollback::{SbCell, ScrollbackBuffer},
6    types::*,
7};
8use crate::framebuffer::{CanvasBuffer, DirtyRectSet, MAX_DIRTY_RECTS};
9use alloc::vec::Vec;
10use core::{
11    fmt,
12    sync::atomic::{AtomicBool, AtomicU64, Ordering},
13};
14
15pub(crate) static PRESENTED_FRAMES: AtomicU64 = AtomicU64::new(0);
16pub(crate) static DOUBLE_BUFFER_MODE: AtomicBool = AtomicBool::new(false);
17pub(crate) static VGA_PRESENT_REGION_COUNT: AtomicU64 = AtomicU64::new(0);
18pub(crate) static VGA_PRESENT_PIXEL_COUNT: AtomicU64 = AtomicU64::new(0);
19
20/// Core framebuffer terminal writer: owns the VGA text-mode state machine,
21/// glyph cache, scrollback ring-buffer, hardware/software cursors, and
22/// pixel-level rendering helpers.
23const SCROLLBAR_W: usize = 12;
24
25use crate::framebuffer::RgbColor;
26
27#[derive(Clone, Copy)]
28pub(crate) struct ClipRect {
29    x: usize,
30    y: usize,
31    w: usize,
32    h: usize,
33}
34
35// Mouse cursor (X arrow, 12×16) ==================================================================================================================================
36use crate::framebuffer::PRESENT_MIN_TICKS;
37
38// Selection clipboard ==========================================================================================================================================================================
39const CLIPBOARD_CAP: usize = 8192;
40static CLIPBOARD: spin::Mutex<([u8; CLIPBOARD_CAP], usize)> =
41    spin::Mutex::new(([0u8; CLIPBOARD_CAP], 0));
42
43pub struct VgaWriter {
44    pub(crate) enabled: bool,
45    /// Shared double-buffered framebuffer canvas (None before init).
46    pub(crate) canvas: Option<CanvasBuffer>,
47    /// Pixel format (pack_rgb/unpack_color helpers).
48    pub(crate) fmt: PixelFormat,
49
50    pub(crate) cols: usize,
51    pub(crate) rows: usize,
52    pub(crate) col: usize,
53    pub(crate) row: usize,
54
55    pub(crate) fg: u32,
56    pub(crate) bg: u32,
57
58    pub(crate) font: &'static [u8],
59    pub(crate) font_info: FontInfo,
60    pub(crate) glyph_mask_cache: Vec<u8>,
61    pub(crate) unicode_map: Vec<(u32, usize)>,
62    pub(crate) status_bar_height: usize,
63    pub(crate) clip: ClipRect,
64
65    /// Scrollback ring-buffer.
66    pub(crate) sb: ScrollbackBuffer,
67    pub(crate) scroll_offset: usize,
68
69    // Live text damage is stored as cells, not repeatedly rasterized pixels.
70    // Rows form a ring: scrolling only rotates the head and clears one row.
71    console_cells: Vec<SbCell>,
72    console_dirty_cells: Vec<bool>,
73    console_row_head: usize,
74    console_pending_scroll: usize,
75    console_pending: bool,
76
77    /// Mouse + text cursor manager.
78    pub(crate) cursor: CursorManager,
79
80    // Text selection
81    pub(crate) sel_active: bool,
82    pub(crate) sel_start_row: usize,
83    pub(crate) sel_start_col: usize,
84    pub(crate) sel_end_row: usize,
85    pub(crate) sel_end_col: usize,
86    pub(crate) prepared_row_cells: Vec<(usize, u32, u32)>,
87
88    /// When true, `end_viewport_render()` defers present : only marks dirty.
89    /// The display server (or explicit `flush_display()`) triggers the actual present.
90    pub(crate) console_defer_present: bool,
91}
92
93unsafe impl Send for VgaWriter {}
94
95/// Short helpers to access the optional canvas without repeating unwrap.
96impl VgaWriter {
97    #[inline]
98    fn can(&self) -> &CanvasBuffer {
99        self.canvas
100            .as_ref()
101            .expect("VgaWriter canvas not initialized")
102    }
103
104    #[inline]
105    fn canm(&mut self) -> &mut CanvasBuffer {
106        self.canvas
107            .as_mut()
108            .expect("VgaWriter canvas not initialized")
109    }
110}
111
112impl VgaWriter {
113    /// Maximum capacity of the scrollback ring buffer.
114    pub(crate) fn sb_capacity(&self) -> usize {
115        self.sb.capacity(self.rows)
116    }
117
118    pub(crate) fn sb_row_at(&self, logical_idx: usize) -> Option<&Vec<SbCell>> {
119        self.sb.row_at(logical_idx)
120    }
121
122    pub(crate) fn sb_push_row(&mut self, row: Vec<SbCell>) {
123        let rows = self.rows;
124        self.sb.push_row(row, rows);
125    }
126
127    pub(crate) fn build_glyph_mask_cache(font: &'static [u8], info: &FontInfo) -> Vec<u8> {
128        let glyph_pixels = info.glyph_w.saturating_mul(info.glyph_h);
129        let total_pixels = info.glyph_count.saturating_mul(glyph_pixels);
130        let mut cache = Vec::with_capacity(total_pixels);
131        if glyph_pixels == 0 || info.glyph_count == 0 {
132            return cache;
133        }
134
135        let row_bytes = info.glyph_w.div_ceil(8);
136        for glyph_index in 0..info.glyph_count {
137            let start = info.data_offset + glyph_index * info.bytes_per_glyph;
138            let end = start.saturating_add(info.bytes_per_glyph);
139            if end > font.len() {
140                cache.resize(cache.len().saturating_add(glyph_pixels), 0);
141                continue;
142            }
143            let glyph_ptr = font[start..end].as_ptr();
144            for gy in 0..info.glyph_h {
145                for gx in 0..info.glyph_w {
146                    let byte = unsafe { *glyph_ptr.add(gy * row_bytes + gx / 8) };
147                    let mask = 0x80u8 >> (gx % 8);
148                    cache.push(if (byte & mask) != 0 { 1 } else { 0 });
149                }
150            }
151        }
152        cache
153    }
154
155    pub(crate) fn glyph_mask_slice(&self, glyph_index: usize) -> Option<&[u8]> {
156        let glyph_pixels = self
157            .font_info
158            .glyph_w
159            .saturating_mul(self.font_info.glyph_h);
160        if glyph_pixels == 0 {
161            return None;
162        }
163        let start = glyph_index.checked_mul(glyph_pixels)?;
164        let end = start.checked_add(glyph_pixels)?;
165        self.glyph_mask_cache.get(start..end)
166    }
167
168    /// Creates a new instance.
169    pub const fn new() -> Self {
170        Self {
171            enabled: false,
172            canvas: None,
173            fmt: PixelFormat {
174                bpp: 0,
175                red_size: 0,
176                red_shift: 0,
177                green_size: 0,
178                green_shift: 0,
179                blue_size: 0,
180                blue_shift: 0,
181            },
182            cols: 0,
183            rows: 0,
184            col: 0,
185            row: 0,
186            fg: 0,
187            bg: 0,
188            font: &[],
189            font_info: FontInfo {
190                glyph_count: 0,
191                bytes_per_glyph: 0,
192                glyph_w: 8,
193                glyph_h: 16,
194                data_offset: 0,
195                unicode_table_offset: None,
196            },
197            glyph_mask_cache: Vec::new(),
198            unicode_map: Vec::new(),
199            status_bar_height: 0,
200            clip: ClipRect {
201                x: 0,
202                y: 0,
203                w: 0,
204                h: 0,
205            },
206            sb: ScrollbackBuffer::new(),
207            scroll_offset: 0,
208            console_cells: Vec::new(),
209            console_dirty_cells: Vec::new(),
210            console_row_head: 0,
211            console_pending_scroll: 0,
212            console_pending: false,
213            cursor: CursorManager::new(),
214            sel_active: false,
215            sel_start_row: 0,
216            sel_start_col: 0,
217            sel_end_row: 0,
218            sel_end_col: 0,
219            prepared_row_cells: Vec::new(),
220            console_defer_present: false,
221        }
222    }
223
224    /// Performs the configure operation.
225    pub(crate) fn configure(
226        &mut self,
227        fb_addr: *mut u8,
228        fb_width: usize,
229        fb_height: usize,
230        pitch: usize,
231        fmt: PixelFormat,
232    ) -> bool {
233        let Some(font_info) = parse_psf(FONT_PSF) else {
234            return false;
235        };
236        let status_bar_height = font_info.glyph_h;
237        let text_height = fb_height.saturating_sub(status_bar_height);
238        let cols = fb_width / font_info.glyph_w;
239        let rows = text_height / font_info.glyph_h;
240        if cols == 0 || rows == 0 {
241            return false;
242        }
243        let (fr, fg, fb) = color_to_rgb(Color::LightGrey);
244        let (br, bg, bb) = color_to_rgb(Color::Black);
245
246        self.enabled = true;
247        self.canvas = Some(CanvasBuffer {
248            addr: fb_addr,
249            width: fb_width,
250            height: fb_height,
251            pitch,
252            bpp: fmt.bpp,
253            back_buffer: None,
254            draw_to_back: false,
255            dirty: DirtyRectSet::empty(),
256            track_dirty: false,
257            present_pending: false,
258            last_present_tick: 0,
259            ops: crate::framebuffer::FramebufferOps::detect(),
260            present_row_buf: None,
261        });
262        self.fmt = fmt;
263        self.cols = cols;
264        self.rows = rows;
265        self.col = 0;
266        self.row = 0;
267        self.fg = fmt.pack_rgb(fr, fg, fb);
268        self.bg = fmt.pack_rgb(br, bg, bb);
269        self.font = FONT_PSF;
270        self.font_info = font_info;
271        self.glyph_mask_cache = Self::build_glyph_mask_cache(FONT_PSF, &self.font_info);
272        self.unicode_map = parse_psf2_unicode_map(FONT_PSF, &self.font_info);
273        self.status_bar_height = status_bar_height;
274        self.clip = ClipRect {
275            x: 0,
276            y: 0,
277            w: fb_width,
278            h: fb_height,
279        };
280        self.sb = ScrollbackBuffer::new();
281        self.scroll_offset = 0;
282        self.console_cells = alloc::vec![SbCell { ch: ' ', fg: self.fg, bg: self.bg }; cols * rows];
283        self.console_dirty_cells = alloc::vec![false; cols * rows];
284        self.console_row_head = 0;
285        self.console_pending_scroll = 0;
286        self.console_pending = false;
287        self.cursor = CursorManager::new();
288        self.cursor.tc_visible = false;
289        self.cursor.tc_col = 0;
290        self.cursor.tc_row = 0;
291        self.cursor.tc_w = 0;
292        self.cursor.tc_h = 0;
293        self.cursor.tc_color = 0;
294        self.sel_active = false;
295        self.prepared_row_cells = Vec::new();
296        true
297    }
298
299    /// Performs the pack color operation.
300    #[inline]
301    pub(crate) fn pack_color(&self, color: RgbColor) -> u32 {
302        self.fmt.pack_rgb(color.r, color.g, color.b)
303    }
304
305    /// Performs the unpack color operation.
306    pub(crate) fn unpack_color(&self, value: u32) -> RgbColor {
307        /// Performs the unscale operation.
308        fn unscale(v: u32, bits: u8) -> u8 {
309            if bits == 0 {
310                return 0;
311            }
312            let max = (1u32 << bits) - 1;
313            ((v * 255) / max) as u8
314        }
315
316        let r = unscale(
317            (value >> self.fmt.red_shift) & ((1u32 << self.fmt.red_size) - 1),
318            self.fmt.red_size,
319        );
320        let g = unscale(
321            (value >> self.fmt.green_shift) & ((1u32 << self.fmt.green_size) - 1),
322            self.fmt.green_size,
323        );
324        let b = unscale(
325            (value >> self.fmt.blue_shift) & ((1u32 << self.fmt.blue_size) - 1),
326            self.fmt.blue_size,
327        );
328        RgbColor::new(r, g, b)
329    }
330
331    /// Sets color.
332    pub fn set_color(&mut self, fg: Color, bg: Color) {
333        self.set_rgb_color(fg.into(), bg.into());
334    }
335
336    /// Sets rgb color.
337    pub fn set_rgb_color(&mut self, fg: RgbColor, bg: RgbColor) {
338        self.fg = self.pack_color(fg);
339        self.bg = self.pack_color(bg);
340    }
341
342    /// Performs the text colors operation.
343    pub fn text_colors(&self) -> (RgbColor, RgbColor) {
344        (self.unpack_color(self.fg), self.unpack_color(self.bg))
345    }
346
347    /// Performs the width operation.
348    pub fn width(&self) -> usize {
349        self.can().width
350    }
351
352    /// Performs the height operation.
353    pub fn height(&self) -> usize {
354        self.can().height
355    }
356
357    /// Performs the cols operation.
358    pub fn cols(&self) -> usize {
359        self.cols
360    }
361
362    /// Performs the rows operation.
363    pub fn rows(&self) -> usize {
364        self.rows
365    }
366
367    /// Performs the glyph size operation.
368    pub fn glyph_size(&self) -> (usize, usize) {
369        (self.font_info.glyph_w, self.font_info.glyph_h)
370    }
371
372    /// Sets cursor cell.
373    pub fn set_cursor_cell(&mut self, col: usize, row: usize) {
374        if !self.enabled || self.cols == 0 || self.rows == 0 {
375            return;
376        }
377        if self.cursor.tc_visible {
378            self.text_cursor_erase_hw();
379            self.cursor.tc_visible = false;
380            self.cursor.tc_dirty = true;
381        }
382        self.col = core::cmp::min(col, self.cols - 1);
383        self.row = core::cmp::min(row, self.rows - 1);
384    }
385
386    /// Performs the text area height operation.
387    pub(crate) fn text_area_height(&self) -> usize {
388        self.can().height.saturating_sub(self.status_bar_height)
389    }
390
391    /// Performs the enabled operation.
392    pub fn enabled(&self) -> bool {
393        self.enabled
394    }
395
396    /// Performs the framebuffer info operation.
397    pub fn framebuffer_info(&self) -> FramebufferInfo {
398        FramebufferInfo {
399            available: self.enabled,
400            width: self.can().width,
401            height: self.can().height,
402            pitch: self.can().pitch,
403            bpp: self.fmt.bpp,
404            red_size: self.fmt.red_size,
405            red_shift: self.fmt.red_shift,
406            green_size: self.fmt.green_size,
407            green_shift: self.fmt.green_shift,
408            blue_size: self.fmt.blue_size,
409            blue_shift: self.fmt.blue_shift,
410            text_cols: self.cols,
411            text_rows: self.rows,
412            glyph_w: self.font_info.glyph_w,
413            glyph_h: self.font_info.glyph_h,
414            double_buffer_mode: DOUBLE_BUFFER_MODE.load(Ordering::Relaxed),
415            double_buffer_enabled: self.can().draw_to_back && self.can().back_buffer.is_some(),
416            ui_scale: current_ui_scale(),
417        }
418    }
419
420    /// Performs the in clip operation.
421    #[inline]
422    pub(crate) fn in_clip(&self, x: usize, y: usize) -> bool {
423        x >= self.clip.x
424            && y >= self.clip.y
425            && x < self.clip.x.saturating_add(self.clip.w)
426            && y < self.clip.y.saturating_add(self.clip.h)
427    }
428
429    /// Performs the clipped rect operation.
430    pub(crate) fn clipped_rect(
431        &self,
432        x: usize,
433        y: usize,
434        width: usize,
435        height: usize,
436    ) -> Option<(usize, usize, usize, usize)> {
437        if width == 0 || height == 0 || !self.enabled {
438            return None;
439        }
440        let src_x2 = core::cmp::min(x.saturating_add(width), self.can().width);
441        let src_y2 = core::cmp::min(y.saturating_add(height), self.can().height);
442        let clip_x2 = self.clip.x.saturating_add(self.clip.w);
443        let clip_y2 = self.clip.y.saturating_add(self.clip.h);
444
445        let sx = core::cmp::max(x, self.clip.x);
446        let sy = core::cmp::max(y, self.clip.y);
447        let ex = core::cmp::min(src_x2, clip_x2);
448        let ey = core::cmp::min(src_y2, clip_y2);
449        if ex <= sx || ey <= sy {
450            return None;
451        }
452        Some((sx, sy, ex - sx, ey - sy))
453    }
454
455    /// Performs the clear dirty operation.
456    pub(crate) fn clear_dirty(&mut self) {
457        self.canm().dirty.clear();
458    }
459
460    /// Performs the mark dirty rect operation.
461    pub(crate) fn mark_dirty_rect(&mut self, x: usize, y: usize, width: usize, height: usize) {
462        let canvas = self.can();
463        if !canvas.track_dirty || width == 0 || height == 0 || !self.enabled {
464            return;
465        }
466        let fw = canvas.width;
467        let fh = canvas.height;
468        let src_x2 = core::cmp::min(x.saturating_add(width), fw);
469        let src_y2 = core::cmp::min(y.saturating_add(height), fh);
470        let clip_x2 = self.clip.x.saturating_add(self.clip.w);
471        let clip_y2 = self.clip.y.saturating_add(self.clip.h);
472        let sx = core::cmp::max(x, self.clip.x);
473        let sy = core::cmp::max(y, self.clip.y);
474        let ex = core::cmp::min(src_x2, clip_x2);
475        let ey = core::cmp::min(src_y2, clip_y2);
476        if ex <= sx || ey <= sy {
477            return;
478        }
479        let _ = canvas;
480        self.canm()
481            .dirty
482            .include(sx as u32, sy as u32, (ex - sx) as u32, (ey - sy) as u32);
483    }
484
485    /// Sets clip rect.
486    pub fn set_clip_rect(&mut self, x: usize, y: usize, width: usize, height: usize) {
487        self.render_pending_console();
488        let x_end = core::cmp::min(x.saturating_add(width), self.can().width);
489        let y_end = core::cmp::min(y.saturating_add(height), self.can().height);
490        self.clip = ClipRect {
491            x,
492            y,
493            w: x_end.saturating_sub(x),
494            h: y_end.saturating_sub(y),
495        };
496    }
497
498    /// Performs the reset clip rect operation.
499    pub fn reset_clip_rect(&mut self) {
500        self.render_pending_console();
501        self.clip = ClipRect {
502            x: 0,
503            y: 0,
504            w: self.can().width,
505            h: self.can().height,
506        };
507    }
508
509    /// Performs the draw to back buffer operation.
510    pub(crate) fn draw_to_back_buffer(&self) -> bool {
511        self.can().draw_to_back && self.can().back_buffer.is_some()
512    }
513
514    /// Enables double buffer.
515    pub fn enable_double_buffer(&mut self) -> bool {
516        self.render_pending_console();
517        if !self.enabled {
518            return false;
519        }
520        if self.can().back_buffer.is_none() {
521            let mut buf = Vec::with_capacity(self.can().width.saturating_mul(self.can().height));
522            for y in 0..self.can().height {
523                for x in 0..self.can().width {
524                    buf.push(self.read_hw_pixel_packed(x, y));
525                }
526            }
527            self.canm().back_buffer = Some(buf);
528        }
529        self.canm().draw_to_back = true;
530        self.canm().track_dirty = true;
531        // Preserve text damage already materialized before the graphics frame.
532        true
533    }
534
535    /// Disables double buffer.
536    pub fn disable_double_buffer(&mut self, present: bool) {
537        self.render_pending_console();
538        if present {
539            self.present_inner(true);
540        }
541        self.canm().draw_to_back = false;
542        self.canm().track_dirty = false;
543        self.clear_dirty();
544    }
545
546    /// Performs the present operation.
547    pub fn present(&mut self) {
548        self.present_inner(false);
549    }
550
551    fn present_inner(&mut self, force: bool) {
552        if !self.enabled {
553            return;
554        }
555
556        if !self.console_pending && self.can().track_dirty && self.can().dirty.len == 0 {
557            self.canm().present_pending = false;
558            return;
559        }
560
561        // Gate both rasterization and presentation, not just framebuffer copies.
562        let now = crate::process::scheduler::ticks();
563        if !force
564            && now != 0
565            && now.saturating_sub(self.can().last_present_tick) < PRESENT_MIN_TICKS
566        {
567            self.canm().present_pending = true;
568            return;
569        }
570
571        // Rasterize only the final visible cells for this frame. Intermediate
572        // scroll positions and overwritten characters never reach the framebuffer.
573        self.render_pending_console();
574
575        // Extract all info through can() first to avoid borrow conflicts
576        let bpp = self.fmt.bpp;
577        let fb_addr = self.can().addr;
578        let pitch = self.can().pitch;
579        let fb_width = self.can().width;
580        let track_dirty = self.can().track_dirty;
581
582        // Early return: nothing dirty to present
583        if track_dirty && self.can().dirty.len == 0 {
584            return;
585        }
586
587        // Copy dirty rects to local array (no borrow on self after this)
588        let region_count;
589        let mut regions = [ClipRect {
590            x: 0,
591            y: 0,
592            w: 0,
593            h: 0,
594        }; MAX_DIRTY_RECTS];
595        if track_dirty {
596            region_count = self.can().dirty.len;
597            for idx in 0..region_count {
598                let r = self.can().dirty.rects[idx];
599                if r.is_valid() {
600                    regions[idx] = ClipRect {
601                        x: r.x0 as usize,
602                        y: r.y0 as usize,
603                        w: r.width() as usize,
604                        h: r.height() as usize,
605                    };
606                }
607            }
608        } else {
609            region_count = 1;
610            regions[0] = ClipRect {
611                x: 0,
612                y: 0,
613                w: fb_width,
614                h: self.can().height,
615            };
616        }
617
618        // Scoped block: get raw ptr to back_buffer then release immutable borrow
619        let buf_ptr = {
620            let Some(buf) = self.can().back_buffer.as_ref() else {
621                return;
622            };
623            buf.as_ptr()
624        };
625
626        // Pixel copy loop using local vars only (no live borrow on self)
627        for region_idx in 0..region_count {
628            let region = regions[region_idx];
629            if region.w == 0 || region.h == 0 {
630                continue;
631            }
632
633            if bpp == 32 {
634                let row_bytes = region.w * 4;
635                if region.x == 0 && region.w == fb_width && pitch == row_bytes {
636                    // Full-width scroll damage is contiguous in both buffers.
637                    unsafe {
638                        core::ptr::copy_nonoverlapping(
639                            buf_ptr.add(region.y * fb_width) as *const u8,
640                            fb_addr.add(region.y * pitch),
641                            row_bytes * region.h,
642                        );
643                    }
644                } else {
645                    for y in region.y..(region.y + region.h) {
646                        let src = unsafe { buf_ptr.add(y * fb_width + region.x) as *const u8 };
647                        let dst_off = y * pitch + region.x * 4;
648                        unsafe {
649                            core::ptr::copy_nonoverlapping(src, fb_addr.add(dst_off), row_bytes);
650                        }
651                    }
652                }
653            } else {
654                // 24bpp: convert row-by-row to packed bytes, then bulk-copy.
655                // Stack buffer avoids heap allocation; falls back to heap
656                // for resolutions wider than 4K (3840 px).
657                let row_bytes = region.w * 3;
658                const MAX_STACK_ROW: usize = 3840 * 3;
659                if row_bytes <= MAX_STACK_ROW {
660                    let mut row_buf = [0u8; MAX_STACK_ROW];
661                    for y in region.y..(region.y + region.h) {
662                        let src_row = y * fb_width + region.x;
663                        for x in 0..region.w {
664                            let packed = unsafe { *buf_ptr.add(src_row + x) };
665                            let off = x * 3;
666                            row_buf[off] = packed as u8;
667                            row_buf[off + 1] = (packed >> 8) as u8;
668                            row_buf[off + 2] = (packed >> 16) as u8;
669                        }
670                        let dst_off = y * pitch + region.x * 3;
671                        unsafe {
672                            core::ptr::copy_nonoverlapping(
673                                row_buf.as_ptr(),
674                                fb_addr.add(dst_off),
675                                row_bytes,
676                            );
677                        }
678                    }
679                } else {
680                    let mut row_buf = alloc::vec![0u8; row_bytes];
681                    for y in region.y..(region.y + region.h) {
682                        let src_row = y * fb_width + region.x;
683                        for x in 0..region.w {
684                            let packed = unsafe { *buf_ptr.add(src_row + x) };
685                            let off = x * 3;
686                            row_buf[off] = packed as u8;
687                            row_buf[off + 1] = (packed >> 8) as u8;
688                            row_buf[off + 2] = (packed >> 16) as u8;
689                        }
690                        let dst_off = y * pitch + region.x * 3;
691                        unsafe {
692                            core::ptr::copy_nonoverlapping(
693                                row_buf.as_ptr(),
694                                fb_addr.add(dst_off),
695                                row_bytes,
696                            );
697                        }
698                    }
699                }
700            }
701
702            VGA_PRESENT_REGION_COUNT.fetch_add(1, Ordering::Relaxed);
703            VGA_PRESENT_PIXEL_COUNT
704                .fetch_add(region.w.saturating_mul(region.h) as u64, Ordering::Relaxed);
705        }
706
707        PRESENTED_FRAMES.fetch_add(1, Ordering::Relaxed);
708        self.clear_dirty();
709        self.canm().present_pending = false;
710        self.canm().last_present_tick = crate::process::scheduler::ticks();
711
712        if crate::hardware::virtio::gpu::is_available() {
713            if let Some(gpu) = crate::hardware::virtio::gpu::get_gpu() {
714                gpu.flush_now();
715            }
716        }
717    }
718
719    pub(crate) fn request_present(&mut self) {
720        if !self.draw_to_back_buffer() {
721            return;
722        }
723        self.canm().present_pending = true;
724        // Early boot has no ticking renderer yet; retain synchronous visibility.
725        self.present_if_due(crate::process::scheduler::ticks() == 0);
726    }
727
728    pub(crate) fn present_if_due(&mut self, force: bool) {
729        if !self.canm().present_pending || !self.draw_to_back_buffer() {
730            return;
731        }
732        let now = crate::process::scheduler::ticks();
733        if force || now.saturating_sub(self.canm().last_present_tick) >= PRESENT_MIN_TICKS {
734            self.present_inner(force);
735        }
736    }
737
738    // Mouse cursor ====================
739
740    /// Performs the mc save hw operation.
741    /// Uses read_pixel_packed (back-buffer aware) instead of read_hw_pixel_packed.
742    pub(crate) fn mc_save_hw(&mut self) {
743        let x = self.cursor.mc_x;
744        let y = self.cursor.mc_y;
745        let fw = self.can().width;
746        let fh = self.can().height;
747        for cy in 0..CURSOR_H {
748            for cx in 0..CURSOR_W {
749                let px = x + cx as i32;
750                let py = y + cy as i32;
751                if px < 0 || py < 0 || px as usize >= fw || py as usize >= fh {
752                    self.cursor.mc_save[cy * CURSOR_W + cx] = 0;
753                    continue;
754                }
755                self.cursor.mc_save[cy * CURSOR_W + cx] =
756                    self.read_pixel_packed(px as usize, py as usize);
757            }
758        }
759    }
760
761    /// Performs the mc draw hw operation.
762    /// Uses put_pixel_raw (back-buffer aware) instead of write_hw_pixel_packed.
763    pub(crate) fn mc_draw_hw(&mut self) {
764        let x = self.cursor.mc_x;
765        let y = self.cursor.mc_y;
766        let black = self.pack_color(RgbColor::BLACK);
767        let white = self.pack_color(RgbColor::WHITE);
768        for cy in 0..CURSOR_H {
769            for cx in 0..CURSOR_W {
770                let p = CURSOR_PIXELS[cy * CURSOR_W + cx];
771                if p == 0 {
772                    continue;
773                }
774                let px = x + cx as i32;
775                let py = y + cy as i32;
776                if px < 0
777                    || py < 0
778                    || px as usize >= self.can().width
779                    || py as usize >= self.can().height
780                {
781                    continue;
782                }
783                let color = if p == 1 { black } else { white };
784                self.put_pixel_raw(px as usize, py as usize, color);
785            }
786        }
787    }
788
789    /// Performs the mc erase hw operation.
790    /// Uses put_pixel_raw (back-buffer aware) instead of write_hw_pixel_packed.
791    pub(crate) fn mc_erase_hw(&mut self) {
792        let x = self.cursor.mc_x;
793        let y = self.cursor.mc_y;
794        for cy in 0..CURSOR_H {
795            for cx in 0..CURSOR_W {
796                if CURSOR_PIXELS[cy * CURSOR_W + cx] == 0 {
797                    continue;
798                }
799                let px = x + cx as i32;
800                let py = y + cy as i32;
801                if px < 0
802                    || py < 0
803                    || px as usize >= self.can().width
804                    || py as usize >= self.can().height
805                {
806                    continue;
807                }
808                self.put_pixel_raw(
809                    px as usize,
810                    py as usize,
811                    self.cursor.mc_save[cy * CURSOR_W + cx],
812                );
813            }
814        }
815    }
816
817    /// Updates mouse cursor.
818    pub fn update_mouse_cursor(&mut self, x: i32, y: i32) {
819        if !self.enabled {
820            return;
821        }
822        if self.cursor.mc_visible && self.cursor.mc_x == x && self.cursor.mc_y == y {
823            self.present_if_due(false);
824            return;
825        }
826        if self.cursor.mc_visible {
827            self.mc_erase_hw();
828        }
829        self.cursor.mc_x = x;
830        self.cursor.mc_y = y;
831        self.mc_save_hw();
832        self.mc_draw_hw();
833        self.cursor.mc_visible = true;
834        self.request_present();
835    }
836
837    /// Performs the hide mouse cursor operation.
838    pub fn hide_mouse_cursor(&mut self) {
839        if self.cursor.mc_visible {
840            self.mc_erase_hw();
841            self.cursor.mc_visible = false;
842        }
843        self.request_present();
844    }
845
846    pub(crate) fn text_cursor_rect(&self) -> Option<(usize, usize, usize, usize)> {
847        if !self.enabled {
848            return None;
849        }
850        let (gw, gh) = self.glyph_size();
851        if gw == 0 || gh == 0 {
852            return None;
853        }
854        let x = self.cursor.tc_col.saturating_mul(gw);
855        let y = self.cursor.tc_row.saturating_mul(gh);
856        if x >= self.can().width || y >= self.can().height {
857            return None;
858        }
859        Some((
860            x,
861            y,
862            gw.min(TEXT_CURSOR_MAX_DIM).min(self.can().width - x),
863            gh.min(TEXT_CURSOR_MAX_DIM).min(self.can().height - y),
864        ))
865    }
866
867    /// Save text cursor pixels. Uses read_pixel_packed (back-buffer aware).
868    pub(crate) fn text_cursor_save_hw(&mut self) {
869        let Some((x, y, w, h)) = self.text_cursor_rect() else {
870            self.cursor.tc_w = 0;
871            self.cursor.tc_h = 0;
872            return;
873        };
874        self.cursor.tc_w = w;
875        self.cursor.tc_h = h;
876        for cy in 0..h {
877            for cx in 0..w {
878                self.cursor.tc_save[cy * TEXT_CURSOR_MAX_DIM + cx] =
879                    self.read_pixel_packed(x + cx, y + cy);
880            }
881        }
882    }
883
884    /// Draw text cursor. Uses put_pixel_raw (back-buffer aware).
885    pub(crate) fn text_cursor_draw_hw(&mut self) {
886        let Some((x, y, w, h)) = self.text_cursor_rect() else {
887            return;
888        };
889        for cy in 0..h {
890            for cx in 0..w {
891                self.put_pixel_raw(x + cx, y + cy, self.cursor.tc_color);
892            }
893        }
894    }
895
896    /// Erase text cursor (restore saved pixels). Uses put_pixel_raw (back-buffer aware).
897    pub(crate) fn text_cursor_erase_hw(&mut self) {
898        let Some((x, y, w, h)) = self.text_cursor_rect() else {
899            return;
900        };
901        let restore_w = w.min(self.cursor.tc_w);
902        let restore_h = h.min(self.cursor.tc_h);
903        for cy in 0..restore_h {
904            for cx in 0..restore_w {
905                self.put_pixel_raw(
906                    x + cx,
907                    y + cy,
908                    self.cursor.tc_save[cy * TEXT_CURSOR_MAX_DIM + cx],
909                );
910            }
911        }
912    }
913
914    pub(crate) fn draw_text_cursor_overlay(&mut self, color: RgbColor) {
915        if !self.enabled {
916            return;
917        }
918        let packed = self.pack_color(color);
919        if self.cursor.tc_visible {
920            self.text_cursor_erase_hw();
921            self.cursor.tc_visible = false;
922            self.cursor.tc_dirty = true;
923        }
924        if packed == self.bg {
925            self.present();
926            return;
927        }
928        self.cursor.tc_col = self.col;
929        self.cursor.tc_row = self.row;
930        self.cursor.tc_color = packed;
931        self.text_cursor_save_hw();
932        self.text_cursor_draw_hw();
933        self.cursor.tc_visible = true;
934        // Present immediately so the cursor is always visible after this call.
935        // Using present() instead of present_if_due() avoids the race where
936        // vgabuf_flush_to_framebuffer presents between draw and next blink.
937        self.present();
938    }
939
940    /// Performs the hide text cursor operation on the writer.
941    pub fn hide_text_cursor(&mut self) {
942        if self.cursor.tc_visible {
943            self.text_cursor_erase_hw();
944            self.cursor.tc_visible = false;
945            self.present();
946        }
947    }
948
949    //  Text selection ==========
950
951    /// Performs the sel normalized operation.
952    pub(crate) fn sel_normalized(&self) -> (usize, usize, usize, usize) {
953        let (sr, sc, er, ec) = (
954            self.sel_start_row,
955            self.sel_start_col,
956            self.sel_end_row,
957            self.sel_end_col,
958        );
959        if sr < er || (sr == er && sc <= ec) {
960            (sr, sc, er, ec)
961        } else {
962            (er, ec, sr, sc)
963        }
964    }
965
966    /// Performs the pixel to sb pos operation.
967    pub fn pixel_to_sb_pos(&self, px: usize, py: usize) -> Option<(usize, usize)> {
968        if !self.enabled {
969            return None;
970        }
971        let gw = self.font_info.glyph_w;
972        let gh = self.font_info.glyph_h;
973        if gw == 0 || gh == 0 {
974            return None;
975        }
976        let text_h = self.text_area_height();
977        let text_w = self.can().width.saturating_sub(SCROLLBAR_W);
978        if px >= text_w || py >= text_h {
979            return None;
980        }
981        let vis_row = py / gh;
982        let vis_col = px / gw;
983        if vis_col >= self.cols {
984            return None;
985        }
986        let total_complete = self.sb.rows.len();
987        let has_partial = !self.sb.cur_row.is_empty();
988        let total_virtual = total_complete + if has_partial { 1 } else { 0 };
989        if total_virtual == 0 {
990            return None;
991        }
992        let view_end = total_virtual.saturating_sub(self.scroll_offset);
993        let view_start = view_end.saturating_sub(self.rows);
994        let display_len = view_end.saturating_sub(view_start);
995        if vis_row >= display_len {
996            return None;
997        }
998        Some((view_start + vis_row, vis_col))
999    }
1000
1001    /// Starts selection.
1002    pub fn start_selection(&mut self, px: usize, py: usize) {
1003        if let Some((row, col)) = self.pixel_to_sb_pos(px, py) {
1004            self.sel_start_row = row;
1005            self.sel_start_col = col;
1006            self.sel_end_row = row;
1007            self.sel_end_col = col;
1008            self.sel_active = true;
1009            self.render_viewport_full();
1010        }
1011    }
1012
1013    /// Updates selection.
1014    pub fn update_selection(&mut self, px: usize, py: usize) {
1015        if !self.sel_active {
1016            return;
1017        }
1018        if let Some((row, col)) = self.pixel_to_sb_pos(px, py) {
1019            if row == self.sel_end_row && col == self.sel_end_col {
1020                return;
1021            }
1022            self.sel_end_row = row;
1023            self.sel_end_col = col;
1024            self.render_viewport_full();
1025        }
1026    }
1027
1028    /// Performs the end selection operation.
1029    pub fn end_selection(&mut self) {
1030        if !self.sel_active {
1031            return;
1032        }
1033        let (start_row, start_col, end_row, end_col) = self.sel_normalized();
1034        let mut bytes: alloc::vec::Vec<u8> = alloc::vec::Vec::new();
1035        for row in start_row..=end_row {
1036            let len = if row < self.sb.rows.len() {
1037                self.sb_row_at(row).map(|r| r.len()).unwrap_or(0)
1038            } else if row == self.sb.rows.len() {
1039                self.sb.cur_row.len()
1040            } else {
1041                break;
1042            };
1043            let c0 = if row == start_row {
1044                start_col.min(len)
1045            } else {
1046                0
1047            };
1048            let c1 = if row == end_row {
1049                end_col.min(len)
1050            } else {
1051                len
1052            };
1053            for col in c0..c1 {
1054                let ch = if row < self.sb.rows.len() {
1055                    self.sb_row_at(row)
1056                        .and_then(|r| r.get(col))
1057                        .map(|cell| cell.ch)
1058                        .unwrap_or(' ')
1059                } else {
1060                    self.sb.cur_row[col].ch
1061                };
1062                let mut buf = [0u8; 4];
1063                bytes.extend_from_slice(ch.encode_utf8(&mut buf).as_bytes());
1064            }
1065            if row < end_row {
1066                bytes.push(b'\n');
1067            }
1068        }
1069        if let Some(mut clip) = CLIPBOARD.try_lock() {
1070            let n = bytes.len().min(CLIPBOARD_CAP);
1071            clip.0[..n].copy_from_slice(&bytes[..n]);
1072            clip.1 = n;
1073        }
1074    }
1075
1076    /// Performs the clear selection operation.
1077    pub fn clear_selection(&mut self) {
1078        if self.sel_active {
1079            self.sel_active = false;
1080            self.render_viewport_full();
1081        }
1082    }
1083
1084    /// Performs the clear with operation.
1085    pub fn clear_with(&mut self, color: RgbColor) {
1086        if !self.enabled {
1087            return;
1088        }
1089        self.console_pending = false;
1090        self.console_pending_scroll = 0;
1091        self.console_dirty_cells.fill(false);
1092        let packed = self.pack_color(color);
1093        let canvas = self.can();
1094        let (fw, fh) = (canvas.width, canvas.height);
1095
1096        if canvas.draw_to_back && canvas.back_buffer.is_some() {
1097            let _ = canvas;
1098            if let Some(buf) = self.canm().back_buffer.as_mut() {
1099                buf.fill(packed);
1100            }
1101            self.mark_dirty_rect(0, 0, fw, fh);
1102            self.col = 0;
1103            self.row = 0;
1104            return;
1105        }
1106
1107        let _ = canvas;
1108        // Direct HW path: bulk fill per row.
1109        if self.fmt.bpp == 32 {
1110            let canvas = self.can();
1111            let _row_bytes = fw * 4;
1112            for y in 0..fh {
1113                let off = y * canvas.pitch;
1114                unsafe {
1115                    let ptr = canvas.addr.add(off) as *mut u32;
1116                    for x in 0..fw {
1117                        core::ptr::write_volatile(ptr.add(x), packed);
1118                    }
1119                }
1120            }
1121        } else {
1122            for y in 0..fh {
1123                for x in 0..fw {
1124                    self.write_hw_pixel_packed(x, y, packed);
1125                }
1126            }
1127        }
1128        self.col = 0;
1129        self.row = 0;
1130    }
1131
1132    /// Performs the clear operation.
1133    pub fn clear(&mut self) {
1134        self.clear_with(self.unpack_color(self.bg));
1135    }
1136
1137    /// Performs the pixel offset operation.
1138    #[inline]
1139    pub(crate) fn pixel_offset(&self, x: usize, y: usize) -> Option<usize> {
1140        if x >= self.can().width || y >= self.can().height {
1141            return None;
1142        }
1143        let bytes_pp = self.fmt.bpp as usize / 8;
1144        let row = y.checked_mul(self.can().pitch)?;
1145        let col = x.checked_mul(bytes_pp)?;
1146        row.checked_add(col)
1147    }
1148
1149    /// Writes hw pixel packed.
1150    pub(crate) fn write_hw_pixel_packed(&mut self, x: usize, y: usize, color: u32) {
1151        let Some(off) = self.pixel_offset(x, y) else {
1152            return;
1153        };
1154        unsafe {
1155            match self.fmt.bpp {
1156                32 => {
1157                    core::ptr::write_volatile(self.can().addr.add(off) as *mut u32, color);
1158                }
1159                24 => {
1160                    core::ptr::write_volatile(self.can().addr.add(off), (color & 0xFF) as u8);
1161                    core::ptr::write_volatile(
1162                        self.can().addr.add(off + 1),
1163                        ((color >> 8) & 0xFF) as u8,
1164                    );
1165                    core::ptr::write_volatile(
1166                        self.can().addr.add(off + 2),
1167                        ((color >> 16) & 0xFF) as u8,
1168                    );
1169                }
1170                _ => {}
1171            }
1172        }
1173    }
1174
1175    /// Reads hw pixel packed.
1176    pub(crate) fn read_hw_pixel_packed(&self, x: usize, y: usize) -> u32 {
1177        let Some(off) = self.pixel_offset(x, y) else {
1178            return 0;
1179        };
1180        unsafe {
1181            match self.fmt.bpp {
1182                32 => core::ptr::read_volatile(self.can().addr.add(off) as *const u32),
1183                24 => {
1184                    let b0 = core::ptr::read_volatile(self.can().addr.add(off)) as u32;
1185                    let b1 = core::ptr::read_volatile(self.can().addr.add(off + 1)) as u32;
1186                    let b2 = core::ptr::read_volatile(self.can().addr.add(off + 2)) as u32;
1187                    b0 | (b1 << 8) | (b2 << 16)
1188                }
1189                _ => 0,
1190            }
1191        }
1192    }
1193
1194    /// Reads pixel packed.
1195    pub(crate) fn read_pixel_packed(&self, x: usize, y: usize) -> u32 {
1196        if x >= self.can().width || y >= self.can().height {
1197            return 0;
1198        }
1199        if self.draw_to_back_buffer() {
1200            if let Some(buf) = self.can().back_buffer.as_ref() {
1201                return buf[y * self.can().width + x];
1202            }
1203        }
1204        self.read_hw_pixel_packed(x, y)
1205    }
1206
1207    /// Performs the put pixel raw operation.
1208    pub(crate) fn put_pixel_raw(&mut self, x: usize, y: usize, color: u32) {
1209        if !self.enabled {
1210            return;
1211        }
1212        // Fast path : inline canvas bounds + clip check in one go.
1213        let canvas = self.can();
1214        let (fw, fh) = (canvas.width, canvas.height);
1215        if x >= fw || y >= fh || !self.in_clip(x, y) {
1216            return;
1217        }
1218        if canvas.draw_to_back && canvas.back_buffer.is_some() {
1219            let _ = canvas;
1220            if let Some(buf) = self.canm().back_buffer.as_mut() {
1221                buf[y * fw + x] = color;
1222            }
1223            self.mark_dirty_rect(x, y, 1, 1);
1224            return;
1225        }
1226        let _ = canvas;
1227        self.write_hw_pixel_packed(x, y, color);
1228    }
1229
1230    /// Performs the draw pixel operation.
1231    pub fn draw_pixel(&mut self, x: usize, y: usize, color: RgbColor) {
1232        self.render_pending_console();
1233        self.put_pixel_raw(x, y, self.pack_color(color));
1234    }
1235
1236    /// Performs the draw pixel alpha operation.
1237    pub fn draw_pixel_alpha(&mut self, x: usize, y: usize, color: RgbColor, alpha: u8) {
1238        self.render_pending_console();
1239        if !self.enabled
1240            || alpha == 0
1241            || x >= self.can().width
1242            || y >= self.can().height
1243            || !self.in_clip(x, y)
1244        {
1245            return;
1246        }
1247        if alpha == 255 {
1248            self.put_pixel_raw(x, y, self.pack_color(color));
1249            return;
1250        }
1251        let dst = self.unpack_color(self.read_pixel_packed(x, y));
1252        let inv = (255u16).saturating_sub(alpha as u16);
1253        let a = alpha as u16;
1254        let blended = RgbColor::new(
1255            ((color.r as u16 * a + dst.r as u16 * inv + 127) / 255) as u8,
1256            ((color.g as u16 * a + dst.g as u16 * inv + 127) / 255) as u8,
1257            ((color.b as u16 * a + dst.b as u16 * inv + 127) / 255) as u8,
1258        );
1259        self.put_pixel_raw(x, y, self.pack_color(blended));
1260    }
1261
1262    /// Performs the draw line operation.
1263    pub fn draw_line(&mut self, x0: isize, y0: isize, x1: isize, y1: isize, color: RgbColor) {
1264        self.render_pending_console();
1265        let mut x = x0;
1266        let mut y = y0;
1267        let dx = (x1 - x0).abs();
1268        let sx = if x0 < x1 { 1 } else { -1 };
1269        let dy = -(y1 - y0).abs();
1270        let sy = if y0 < y1 { 1 } else { -1 };
1271        let mut err = dx + dy;
1272        let packed = self.pack_color(color);
1273
1274        loop {
1275            if x >= 0 && y >= 0 {
1276                self.put_pixel_raw(x as usize, y as usize, packed);
1277            }
1278            if x == x1 && y == y1 {
1279                break;
1280            }
1281            let e2 = 2 * err;
1282            if e2 >= dy {
1283                err += dy;
1284                x += sx;
1285            }
1286            if e2 <= dx {
1287                err += dx;
1288                y += sy;
1289            }
1290        }
1291    }
1292
1293    /// Performs the draw rect operation.
1294    pub fn draw_rect(&mut self, x: usize, y: usize, width: usize, height: usize, color: RgbColor) {
1295        self.render_pending_console();
1296        if width == 0 || height == 0 {
1297            return;
1298        }
1299        let x2 = x.saturating_add(width - 1);
1300        let y2 = y.saturating_add(height - 1);
1301        self.draw_line(x as isize, y as isize, x2 as isize, y as isize, color);
1302        self.draw_line(x as isize, y as isize, x as isize, y2 as isize, color);
1303        self.draw_line(x2 as isize, y as isize, x2 as isize, y2 as isize, color);
1304        self.draw_line(x as isize, y2 as isize, x2 as isize, y2 as isize, color);
1305    }
1306
1307    /// Performs the fill rect operation.
1308    pub fn fill_rect(&mut self, x: usize, y: usize, width: usize, height: usize, color: RgbColor) {
1309        self.render_pending_console();
1310        let Some((sx, sy, sw, sh)) = self.clipped_rect(x, y, width, height) else {
1311            return;
1312        };
1313        let sw = sw.min(self.can().width.saturating_sub(sx));
1314        let sh = sh.min(self.can().height.saturating_sub(sy));
1315        if sw == 0 || sh == 0 {
1316            return;
1317        }
1318        let packed = self.pack_color(color);
1319
1320        if self.draw_to_back_buffer() {
1321            let fw = self.can().width;
1322            let wrote = self
1323                .canm()
1324                .back_buffer
1325                .as_mut()
1326                .map(|buf| {
1327                    for py in sy..(sy + sh) {
1328                        let row = py * fw;
1329                        let start = row + sx;
1330                        let end = start + sw;
1331                        buf[start..end].fill(packed);
1332                    }
1333                })
1334                .is_some();
1335            if wrote {
1336                self.mark_dirty_rect(sx, sy, sw, sh);
1337            }
1338            return;
1339        }
1340
1341        if self.fmt.bpp == 32 {
1342            for py in sy..(sy + sh) {
1343                let Some(row_off) = py
1344                    .checked_mul(self.can().pitch)
1345                    .and_then(|v| v.checked_add(sx * 4))
1346                else {
1347                    continue;
1348                };
1349                let count = sw;
1350                unsafe {
1351                    let ptr = self.can().addr.add(row_off) as *mut u32;
1352                    for i in 0..count {
1353                        core::ptr::write_volatile(ptr.add(i), packed);
1354                    }
1355                }
1356            }
1357            return;
1358        }
1359
1360        for py in sy..(sy + sh) {
1361            for px in sx..(sx + sw) {
1362                self.write_hw_pixel_packed(px, py, packed);
1363            }
1364        }
1365    }
1366
1367    /// Performs the fill rect alpha operation.
1368    pub fn fill_rect_alpha(
1369        &mut self,
1370        x: usize,
1371        y: usize,
1372        width: usize,
1373        height: usize,
1374        color: RgbColor,
1375        alpha: u8,
1376    ) {
1377        self.render_pending_console();
1378        if !self.enabled || width == 0 || height == 0 || alpha == 0 {
1379            return;
1380        }
1381        if alpha == 255 {
1382            self.fill_rect(x, y, width, height, color);
1383            return;
1384        }
1385        let x_end = core::cmp::min(x.saturating_add(width), self.can().width);
1386        let y_end = core::cmp::min(y.saturating_add(height), self.can().height);
1387        for py in y..y_end {
1388            for px in x..x_end {
1389                self.draw_pixel_alpha(px, py, color, alpha);
1390            }
1391        }
1392    }
1393
1394    /// Performs the blit rgb operation.
1395    pub fn blit_rgb(
1396        &mut self,
1397        dst_x: usize,
1398        dst_y: usize,
1399        src_width: usize,
1400        src_height: usize,
1401        pixels: &[RgbColor],
1402    ) -> bool {
1403        self.render_pending_console();
1404        let len = src_width.saturating_mul(src_height);
1405        if !self.enabled || src_width == 0 || src_height == 0 || pixels.len() < len {
1406            return false;
1407        }
1408        let x_end = core::cmp::min(dst_x.saturating_add(src_width), self.can().width);
1409        let y_end = core::cmp::min(dst_y.saturating_add(src_height), self.can().height);
1410        if x_end <= dst_x || y_end <= dst_y {
1411            return true;
1412        }
1413        let copy_w = x_end - dst_x;
1414        let copy_h = y_end - dst_y;
1415
1416        let fmt = self.fmt;
1417        if self.draw_to_back_buffer() {
1418            let fb_width = self.can().width;
1419            let wrote = self
1420                .canm()
1421                .back_buffer
1422                .as_mut()
1423                .map(|buf| {
1424                    for row in 0..copy_h {
1425                        let src_row = row * src_width;
1426                        let dst_row = (dst_y + row) * fb_width + dst_x;
1427                        for col in 0..copy_w {
1428                            buf[dst_row + col] = fmt.pack_rgb(
1429                                pixels[src_row + col].r,
1430                                pixels[src_row + col].g,
1431                                pixels[src_row + col].b,
1432                            );
1433                        }
1434                    }
1435                })
1436                .is_some();
1437            if wrote {
1438                self.mark_dirty_rect(dst_x, dst_y, copy_w, copy_h);
1439                return true;
1440            }
1441        }
1442
1443        if fmt.bpp == 32 {
1444            let mut row_buf = alloc::vec![0u32; copy_w];
1445            for row in 0..copy_h {
1446                let src_row = row * src_width;
1447                for col in 0..copy_w {
1448                    row_buf[col] = self.pack_color(pixels[src_row + col]);
1449                }
1450                if let Some(off) = self.pixel_offset(dst_x, dst_y + row) {
1451                    unsafe {
1452                        core::ptr::copy_nonoverlapping(
1453                            row_buf.as_ptr(),
1454                            self.can().addr.add(off) as *mut u32,
1455                            copy_w,
1456                        );
1457                    }
1458                }
1459            }
1460        } else {
1461            for row in 0..copy_h {
1462                let src_row = row * src_width;
1463                for col in 0..copy_w {
1464                    self.draw_pixel(dst_x + col, dst_y + row, pixels[src_row + col]);
1465                }
1466            }
1467        }
1468        true
1469    }
1470
1471    /// Performs the blit rgb24 operation.
1472    pub fn blit_rgb24(
1473        &mut self,
1474        dst_x: usize,
1475        dst_y: usize,
1476        src_width: usize,
1477        src_height: usize,
1478        bytes: &[u8],
1479    ) -> bool {
1480        self.render_pending_console();
1481        let needed = src_width.saturating_mul(src_height).saturating_mul(3);
1482        if !self.enabled || src_width == 0 || src_height == 0 || bytes.len() < needed {
1483            return false;
1484        }
1485        let x_end = core::cmp::min(dst_x.saturating_add(src_width), self.can().width);
1486        let y_end = core::cmp::min(dst_y.saturating_add(src_height), self.can().height);
1487        if x_end <= dst_x || y_end <= dst_y {
1488            return true;
1489        }
1490        let copy_w = x_end - dst_x;
1491        let copy_h = y_end - dst_y;
1492
1493        let fmt = self.fmt;
1494        if self.draw_to_back_buffer() {
1495            let fb_width = self.can().width;
1496            let wrote = self
1497                .canm()
1498                .back_buffer
1499                .as_mut()
1500                .map(|buf| {
1501                    for row in 0..copy_h {
1502                        let src_base = row * src_width * 3;
1503                        let dst_row = (dst_y + row) * fb_width + dst_x;
1504                        for col in 0..copy_w {
1505                            let i = src_base + col * 3;
1506                            buf[dst_row + col] = fmt.pack_rgb(bytes[i], bytes[i + 1], bytes[i + 2]);
1507                        }
1508                    }
1509                })
1510                .is_some();
1511            if wrote {
1512                self.mark_dirty_rect(dst_x, dst_y, copy_w, copy_h);
1513                return true;
1514            }
1515        }
1516
1517        if fmt.bpp == 32 {
1518            let mut row_buf = alloc::vec![0u32; copy_w];
1519            for row in 0..copy_h {
1520                let src_base = row * src_width * 3;
1521                for col in 0..copy_w {
1522                    let i = src_base + col * 3;
1523                    row_buf[col] =
1524                        self.pack_color(RgbColor::new(bytes[i], bytes[i + 1], bytes[i + 2]));
1525                }
1526                if let Some(off) = self.pixel_offset(dst_x, dst_y + row) {
1527                    unsafe {
1528                        core::ptr::copy_nonoverlapping(
1529                            row_buf.as_ptr(),
1530                            self.can().addr.add(off) as *mut u32,
1531                            copy_w,
1532                        );
1533                    }
1534                }
1535            }
1536        } else {
1537            for row in 0..copy_h {
1538                for col in 0..copy_w {
1539                    let i = (row * src_width + col) * 3;
1540                    let color = RgbColor::new(bytes[i], bytes[i + 1], bytes[i + 2]);
1541                    self.draw_pixel(dst_x + col, dst_y + row, color);
1542                }
1543            }
1544        }
1545        true
1546    }
1547
1548    /// Performs the blit rgba operation.
1549    pub fn blit_rgba(
1550        &mut self,
1551        dst_x: usize,
1552        dst_y: usize,
1553        src_width: usize,
1554        src_height: usize,
1555        bytes: &[u8],
1556        global_alpha: u8,
1557    ) -> bool {
1558        self.render_pending_console();
1559        let needed = src_width.saturating_mul(src_height).saturating_mul(4);
1560        if !self.enabled
1561            || src_width == 0
1562            || src_height == 0
1563            || bytes.len() < needed
1564            || global_alpha == 0
1565        {
1566            return false;
1567        }
1568
1569        let Some((sx, sy, sw, sh)) = self.clipped_rect(dst_x, dst_y, src_width, src_height) else {
1570            return true;
1571        };
1572        let src_x0 = sx.saturating_sub(dst_x);
1573        let src_y0 = sy.saturating_sub(dst_y);
1574
1575        for row in 0..sh {
1576            let syi = src_y0 + row;
1577            for col in 0..sw {
1578                let sxi = src_x0 + col;
1579                let i = (syi * src_width + sxi) * 4;
1580                let r = bytes[i];
1581                let g = bytes[i + 1];
1582                let b = bytes[i + 2];
1583                let sa = bytes[i + 3];
1584                if sa == 0 {
1585                    continue;
1586                }
1587                let a = ((sa as u16 * global_alpha as u16 + 127) / 255) as u8;
1588                let dx = sx + col;
1589                let dy = sy + row;
1590                if a == 255 {
1591                    self.put_pixel_raw(dx, dy, self.pack_color(RgbColor::new(r, g, b)));
1592                } else if a != 0 {
1593                    self.draw_pixel_alpha(dx, dy, RgbColor::new(r, g, b), a);
1594                }
1595            }
1596        }
1597        true
1598    }
1599
1600    /// Performs the blit sprite rgba operation.
1601    pub fn blit_sprite_rgba(
1602        &mut self,
1603        dst_x: usize,
1604        dst_y: usize,
1605        sprite: SpriteRgba<'_>,
1606        global_alpha: u8,
1607    ) -> bool {
1608        self.render_pending_console();
1609        self.blit_rgba(
1610            dst_x,
1611            dst_y,
1612            sprite.width,
1613            sprite.height,
1614            sprite.pixels,
1615            global_alpha,
1616        )
1617    }
1618
1619    /// Performs the draw text at operation.
1620    pub fn draw_text_at(
1621        &mut self,
1622        pixel_x: usize,
1623        pixel_y: usize,
1624        text: &str,
1625        fg: RgbColor,
1626        bg: RgbColor,
1627    ) {
1628        self.render_pending_console();
1629        if !self.enabled {
1630            return;
1631        }
1632        let gw = self.font_info.glyph_w;
1633        let gh = self.font_info.glyph_h;
1634        let fg_packed = self.pack_color(fg);
1635        let bg_packed = self.pack_color(bg);
1636        let mut cx = pixel_x;
1637        let cy = pixel_y;
1638        for ch in text.chars() {
1639            if ch == '\n' {
1640                break;
1641            }
1642            if cx + gw > self.can().width || cy + gh > self.can().height {
1643                break;
1644            }
1645            self.draw_glyph_at_pixel(cx, cy, ch, fg_packed, bg_packed);
1646            cx += gw;
1647        }
1648    }
1649
1650    /// Performs the glyph index for char operation.
1651    pub(crate) fn glyph_index_for_char(&self, ch: char) -> usize {
1652        if ch.is_ascii() {
1653            let idx = ch as usize;
1654            if idx < self.font_info.glyph_count {
1655                return idx;
1656            }
1657        }
1658        let cp = ch as u32;
1659        if let Some((_, glyph)) = self.unicode_map.iter().find(|(u, _)| *u == cp) {
1660            return *glyph;
1661        }
1662        if let Some((_, glyph)) = self.unicode_map.iter().find(|(u, _)| *u == ('?' as u32)) {
1663            return *glyph;
1664        }
1665        if ('?' as usize) < self.font_info.glyph_count {
1666            return '?' as usize;
1667        }
1668        0
1669    }
1670
1671    /// Performs the draw glyph index at pixel operation.
1672    pub(crate) fn draw_glyph_index_at_pixel(
1673        &mut self,
1674        pixel_x: usize,
1675        pixel_y: usize,
1676        glyph_index: usize,
1677        fg: u32,
1678        bg: u32,
1679    ) {
1680        if !self.enabled {
1681            return;
1682        }
1683        let glyph_index = core::cmp::min(glyph_index, self.font_info.glyph_count.saturating_sub(1));
1684        let gw = self.font_info.glyph_w;
1685        let gh = self.font_info.glyph_h;
1686        let glyph_pixels = gw.saturating_mul(gh);
1687        let Some(mask) = self.glyph_mask_slice(glyph_index) else {
1688            return;
1689        };
1690        let mask_ptr = mask.as_ptr();
1691
1692        if self.draw_to_back_buffer()
1693            && pixel_x + gw <= self.can().width
1694            && pixel_y + gh <= self.can().height
1695        {
1696            let fb_width = self.can().width;
1697            if let Some(buf) = self.canm().back_buffer.as_mut() {
1698                for gy in 0..gh {
1699                    let row_start = (pixel_y + gy) * fb_width + pixel_x;
1700                    buf[row_start..row_start + gw].fill(bg);
1701                    for gx in 0..gw {
1702                        let idx = gy * gw + gx;
1703                        if idx >= glyph_pixels {
1704                            continue;
1705                        }
1706                        let bit = unsafe { *mask_ptr.add(idx) };
1707                        if bit != 0 {
1708                            buf[row_start + gx] = fg;
1709                        }
1710                    }
1711                }
1712            }
1713            self.mark_dirty_rect(pixel_x, pixel_y, gw, gh);
1714        } else {
1715            if self.fmt.bpp == 32 {
1716                for gy in 0..gh {
1717                    if let Some(off) = self.pixel_offset(pixel_x, pixel_y + gy) {
1718                        let row_ptr = unsafe { self.can().addr.add(off) } as *mut u32;
1719                        for gx in 0..gw {
1720                            let idx = gy * gw + gx;
1721                            let color = if idx < glyph_pixels && unsafe { *mask_ptr.add(idx) } != 0
1722                            {
1723                                fg
1724                            } else {
1725                                bg
1726                            };
1727                            unsafe {
1728                                core::ptr::write_volatile(row_ptr.add(gx), color);
1729                            }
1730                        }
1731                    }
1732                }
1733            } else {
1734                for gy in 0..gh {
1735                    for gx in 0..gw {
1736                        let idx = gy * gw + gx;
1737                        if idx >= glyph_pixels {
1738                            continue;
1739                        }
1740                        let color = if unsafe { *mask_ptr.add(idx) } != 0 {
1741                            fg
1742                        } else {
1743                            bg
1744                        };
1745                        self.put_pixel_raw(pixel_x + gx, pixel_y + gy, color);
1746                    }
1747                }
1748            }
1749        }
1750    }
1751
1752    /// Performs the draw glyph at pixel operation.
1753    pub(crate) fn draw_glyph_at_pixel(
1754        &mut self,
1755        pixel_x: usize,
1756        pixel_y: usize,
1757        ch: char,
1758        fg: u32,
1759        bg: u32,
1760    ) {
1761        let glyph_index = self.glyph_index_for_char(ch);
1762        self.draw_glyph_index_at_pixel(pixel_x, pixel_y, glyph_index, fg, bg);
1763    }
1764
1765    pub(crate) fn fill_text_span_bg(
1766        &mut self,
1767        pixel_x: usize,
1768        pixel_y: usize,
1769        width: usize,
1770        height: usize,
1771        bg: u32,
1772    ) {
1773        if self.draw_to_back_buffer()
1774            && pixel_x + width <= self.can().width
1775            && pixel_y + height <= self.can().height
1776        {
1777            let fb_width = self.can().width;
1778            if let Some(buf) = self.canm().back_buffer.as_mut() {
1779                for row in 0..height {
1780                    let start = (pixel_y + row) * fb_width + pixel_x;
1781                    let end = start + width;
1782                    buf[start..end].fill(bg);
1783                }
1784            }
1785            self.mark_dirty_rect(pixel_x, pixel_y, width, height);
1786            return;
1787        }
1788
1789        let gh = height;
1790        let color = self.unpack_color(bg);
1791        self.fill_rect(pixel_x, pixel_y, width, gh, color);
1792    }
1793
1794    pub(crate) fn clear_text_line_pixels(&mut self, vis_row: usize) {
1795        let gh = self.font_info.glyph_h;
1796        let text_w = self.can().width.saturating_sub(SCROLLBAR_W);
1797        self.fill_text_span_bg(0, vis_row.saturating_mul(gh), text_w, gh, self.bg);
1798    }
1799
1800    pub(crate) fn visible_virtual_bounds(&self) -> (usize, usize, usize, usize, bool) {
1801        let total_complete = self.sb.rows.len();
1802        let has_partial = !self.sb.cur_row.is_empty();
1803        let total_virtual = total_complete + if has_partial { 1 } else { 0 };
1804        let view_end = total_virtual.saturating_sub(self.scroll_offset);
1805        let view_start = view_end.saturating_sub(self.rows);
1806        (
1807            total_complete,
1808            total_virtual,
1809            view_start,
1810            view_end,
1811            has_partial,
1812        )
1813    }
1814
1815    pub(crate) fn sync_live_cursor_from_view(
1816        &mut self,
1817        total_complete: usize,
1818        view_start: usize,
1819        view_end: usize,
1820    ) {
1821        let display_len = view_end.saturating_sub(view_start);
1822        self.row = if display_len > 0 { display_len - 1 } else { 0 };
1823        let last_virt = view_start + display_len.saturating_sub(1);
1824        let last_len = if last_virt < total_complete {
1825            self.sb_row_at(last_virt).map(|row| row.len()).unwrap_or(0)
1826        } else if last_virt == total_complete {
1827            self.sb.cur_row.len()
1828        } else {
1829            0
1830        };
1831        self.col = last_len.min(self.cols);
1832    }
1833
1834    pub(crate) fn selection_colors_for_cell(
1835        &self,
1836        virt_row: usize,
1837        col: usize,
1838        fg: u32,
1839        bg: u32,
1840    ) -> (u32, u32) {
1841        if !self.sel_active {
1842            return (fg, bg);
1843        }
1844        let (sel_sr, sel_sc, sel_er, sel_ec) = self.sel_normalized();
1845        let in_sel = if virt_row < sel_sr || virt_row > sel_er {
1846            false
1847        } else if sel_sr == sel_er {
1848            col >= sel_sc && col < sel_ec
1849        } else if virt_row == sel_sr {
1850            col >= sel_sc
1851        } else if virt_row == sel_er {
1852            col < sel_ec
1853        } else {
1854            true
1855        };
1856        if in_sel {
1857            (
1858                self.pack_color(RgbColor::WHITE),
1859                self.pack_color(RgbColor::new(0x26, 0x5F, 0xCC)),
1860            )
1861        } else {
1862            (fg, bg)
1863        }
1864    }
1865
1866    pub(crate) fn render_virtual_row(
1867        &mut self,
1868        virt_row: usize,
1869        vis_row: usize,
1870        total_complete: usize,
1871        has_partial: bool,
1872    ) {
1873        let glyph_h = self.font_info.glyph_h;
1874        let glyph_w = self.font_info.glyph_w;
1875        let py = vis_row.saturating_mul(glyph_h);
1876
1877        let (row_ptr, row_len) = if virt_row < total_complete {
1878            let row = match self.sb_row_at(virt_row) {
1879                Some(row) => row,
1880                None => return,
1881            };
1882            (row.as_ptr(), row.len())
1883        } else if has_partial && virt_row == total_complete {
1884            (self.sb.cur_row.as_ptr(), self.sb.cur_row.len())
1885        } else {
1886            (core::ptr::null(), 0)
1887        };
1888
1889        let cell_count = row_len.min(self.cols);
1890        let text_w = self.can().width.saturating_sub(SCROLLBAR_W);
1891        let used_width = cell_count.saturating_mul(glyph_w).min(text_w);
1892
1893        if self.draw_to_back_buffer() && py + glyph_h <= self.can().height {
1894            let fb_width = self.can().width;
1895            let default_bg = self.bg;
1896            let glyph_pixels = glyph_w.saturating_mul(glyph_h);
1897            let glyph_cache_ptr = self.glyph_mask_cache.as_ptr();
1898            self.prepared_row_cells.clear();
1899            if self.prepared_row_cells.capacity() < cell_count {
1900                self.prepared_row_cells
1901                    .reserve(cell_count - self.prepared_row_cells.capacity());
1902            }
1903            for col in 0..cell_count {
1904                let cell = unsafe { &*row_ptr.add(col) };
1905                let glyph_index = self.glyph_index_for_char(cell.ch);
1906                let (draw_fg, draw_bg) =
1907                    self.selection_colors_for_cell(virt_row, col, cell.fg, cell.bg);
1908                self.prepared_row_cells
1909                    .push((glyph_index, draw_fg, draw_bg));
1910            }
1911            let cells_ptr = self.prepared_row_cells.as_ptr();
1912            let cells_len = self.prepared_row_cells.len();
1913            if let Some(buf) = self.canm().back_buffer.as_mut() {
1914                for gy in 0..glyph_h {
1915                    let row_start = (py + gy) * fb_width;
1916                    buf[row_start..row_start + text_w].fill(default_bg);
1917                }
1918
1919                for col in 0..cells_len {
1920                    let (glyph_index, draw_fg, draw_bg) = unsafe { *cells_ptr.add(col) };
1921                    let px = col.saturating_mul(glyph_w);
1922                    if draw_bg != default_bg {
1923                        for gy in 0..glyph_h {
1924                            let row_start = (py + gy) * fb_width + px;
1925                            buf[row_start..row_start + glyph_w].fill(draw_bg);
1926                        }
1927                    }
1928
1929                    let glyph_base = glyph_index.saturating_mul(glyph_pixels);
1930                    for gy in 0..glyph_h {
1931                        let row_start = (py + gy) * fb_width + px;
1932                        for gx in 0..glyph_w {
1933                            let idx = glyph_base + gy * glyph_w + gx;
1934                            let bit = unsafe { *glyph_cache_ptr.add(idx) };
1935                            if bit != 0 {
1936                                buf[row_start + gx] = draw_fg;
1937                            }
1938                        }
1939                    }
1940                }
1941            }
1942
1943            self.mark_dirty_rect(0, py, text_w, glyph_h);
1944            return;
1945        }
1946
1947        for col in 0..cell_count {
1948            let px = col.saturating_mul(glyph_w);
1949            let cell = unsafe { &*row_ptr.add(col) };
1950            let (draw_fg, draw_bg) =
1951                self.selection_colors_for_cell(virt_row, col, cell.fg, cell.bg);
1952            self.draw_glyph_at_pixel(px, py, cell.ch, draw_fg, draw_bg);
1953        }
1954        if text_w > used_width {
1955            self.fill_text_span_bg(used_width, py, text_w - used_width, glyph_h, self.bg);
1956        }
1957    }
1958
1959    pub(crate) fn redraw_visible_rows(&mut self, start_vis_row: usize, count: usize) {
1960        let (total_complete, _, view_start, view_end, has_partial) = self.visible_virtual_bounds();
1961        let display_len = view_end.saturating_sub(view_start);
1962        let end_vis_row = start_vis_row.saturating_add(count).min(self.rows);
1963        for vis_row in start_vis_row..end_vis_row {
1964            if vis_row < display_len {
1965                self.render_virtual_row(view_start + vis_row, vis_row, total_complete, has_partial);
1966            } else {
1967                self.clear_text_line_pixels(vis_row);
1968            }
1969        }
1970    }
1971
1972    pub(crate) fn ensure_back_buffer_for_viewport(&mut self) {
1973        let canvas = self.can();
1974        if canvas.back_buffer.is_none() {
1975            let total = canvas.width.saturating_mul(canvas.height);
1976            if total > 0 {
1977                let mut buf = alloc::vec![0u32; total];
1978                // Sync back buffer from hardware so content written directly
1979                // (e.g. status bar during boot) survives present().
1980                if self.enabled && !canvas.addr.is_null() {
1981                    if self.fmt.bpp == 32 {
1982                        unsafe {
1983                            for y in 0..canvas.height {
1984                                core::ptr::copy_nonoverlapping(
1985                                    canvas.addr.add(y * canvas.pitch),
1986                                    buf.as_mut_ptr().add(y * canvas.width) as *mut u8,
1987                                    canvas.width * 4,
1988                                );
1989                            }
1990                        }
1991                    } else {
1992                        for y in 0..canvas.height {
1993                            for x in 0..canvas.width {
1994                                buf[y * canvas.width + x] = self.read_hw_pixel_packed(x, y);
1995                            }
1996                        }
1997                    }
1998                }
1999                let _ = canvas;
2000                self.canm().back_buffer = Some(buf);
2001            }
2002        }
2003    }
2004
2005    pub(crate) fn begin_viewport_render(&mut self) -> (bool, bool) {
2006        self.ensure_back_buffer_for_viewport();
2007        let canvas = self.can();
2008        let prev_draw_to_back = canvas.draw_to_back;
2009        let prev_track_dirty = canvas.track_dirty;
2010        let has_back = canvas.back_buffer.is_some();
2011        let _ = canvas;
2012        if has_back {
2013            let canvas = self.canm();
2014            canvas.draw_to_back = true;
2015            canvas.track_dirty = true;
2016            // Damage belongs to the pending frame, not this text fragment.
2017            // Keep it until present() has actually copied it to hardware.
2018        }
2019        (prev_draw_to_back, prev_track_dirty)
2020    }
2021
2022    pub(crate) fn end_viewport_render(&mut self, prev_draw_to_back: bool, prev_track_dirty: bool) {
2023        let has_back = self.can().back_buffer.is_some();
2024        if has_back {
2025            if !self.console_defer_present {
2026                self.request_present();
2027                let now = crate::process::scheduler::ticks();
2028                let force_present = !prev_draw_to_back || !prev_track_dirty || now == 0;
2029                self.present_if_due(force_present);
2030            }
2031            let canvas = self.canm();
2032            canvas.draw_to_back = prev_draw_to_back;
2033            canvas.track_dirty = prev_track_dirty;
2034        }
2035    }
2036
2037    pub(crate) fn finalize_live_view_state(&mut self) {
2038        let (total_complete, _, view_start, view_end, _) = self.visible_virtual_bounds();
2039        if self.scroll_offset == 0 {
2040            self.sync_live_cursor_from_view(total_complete, view_start, view_end);
2041        }
2042    }
2043
2044    pub(crate) fn render_viewport_full(&mut self) {
2045        self.render_pending_console();
2046        let (prev_draw_to_back, prev_track_dirty) = self.begin_viewport_render();
2047        let text_h = self.text_area_height();
2048        let text_w = self.can().width.saturating_sub(SCROLLBAR_W);
2049        self.fill_rect(0, 0, text_w, text_h, self.unpack_color(self.bg));
2050        self.redraw_visible_rows(0, self.rows);
2051        self.finalize_live_view_state();
2052        self.draw_scrollbar_inner();
2053        self.end_viewport_render(prev_draw_to_back, prev_track_dirty);
2054    }
2055
2056    pub(crate) fn refresh_viewport_decorations(&mut self) {
2057        let (prev_draw_to_back, prev_track_dirty) = self.begin_viewport_render();
2058        self.finalize_live_view_state();
2059        self.draw_scrollbar_inner();
2060        self.end_viewport_render(prev_draw_to_back, prev_track_dirty);
2061    }
2062
2063    pub(crate) fn set_scroll_offset_and_render(&mut self, new_offset: usize) {
2064        self.render_pending_console();
2065        let old_offset = self.scroll_offset;
2066        self.scroll_offset = new_offset;
2067        if !self.redraw_from_scrollback_incremental(old_offset) {
2068            self.redraw_from_scrollback();
2069        }
2070    }
2071
2072    pub(crate) fn move_text_view_pixels_up(&mut self, pixels: usize) {
2073        if pixels == 0 {
2074            return;
2075        }
2076        let text_h = self.text_area_height();
2077        let text_w = self.can().width.saturating_sub(SCROLLBAR_W);
2078        if pixels >= text_h {
2079            self.fill_rect(0, 0, text_w, text_h, self.unpack_color(self.bg));
2080            return;
2081        }
2082        let move_rows = text_h - pixels;
2083        if self.draw_to_back_buffer() {
2084            let row_width = self.can().width;
2085            if let Some(buf) = self.canm().back_buffer.as_mut() {
2086                buf.copy_within(pixels * row_width..text_h * row_width, 0);
2087            }
2088            self.mark_dirty_rect(0, 0, row_width, text_h);
2089        } else {
2090            unsafe {
2091                core::ptr::copy(
2092                    self.can().addr.add(pixels * self.can().pitch),
2093                    self.can().addr,
2094                    move_rows * self.can().pitch,
2095                );
2096            }
2097        }
2098        self.fill_rect(0, move_rows, text_w, pixels, self.unpack_color(self.bg));
2099    }
2100
2101    pub(crate) fn move_text_view_pixels_down(&mut self, pixels: usize) {
2102        if pixels == 0 {
2103            return;
2104        }
2105        let text_h = self.text_area_height();
2106        let text_w = self.can().width.saturating_sub(SCROLLBAR_W);
2107        if pixels >= text_h {
2108            self.fill_rect(0, 0, text_w, text_h, self.unpack_color(self.bg));
2109            return;
2110        }
2111        let move_rows = text_h - pixels;
2112        if self.draw_to_back_buffer() {
2113            let row_width = self.can().width;
2114            if let Some(buf) = self.canm().back_buffer.as_mut() {
2115                buf.copy_within(0..move_rows * row_width, pixels * row_width);
2116            }
2117            self.mark_dirty_rect(0, 0, row_width, text_h);
2118        } else {
2119            unsafe {
2120                core::ptr::copy(
2121                    self.can().addr,
2122                    self.can().addr.add(pixels * self.can().pitch),
2123                    move_rows * self.can().pitch,
2124                );
2125            }
2126        }
2127        self.fill_rect(0, 0, text_w, pixels, self.unpack_color(self.bg));
2128    }
2129
2130    /// Performs the layout text lines operation.
2131    pub(crate) fn layout_text_lines(
2132        &self,
2133        text: &str,
2134        wrap: bool,
2135        max_cols: Option<usize>,
2136    ) -> Vec<Vec<char>> {
2137        let mut lines: Vec<Vec<char>> = Vec::new();
2138        let mut current: Vec<char> = Vec::new();
2139        let wrap_cols = max_cols.filter(|&c| c > 0);
2140
2141        for ch in text.chars() {
2142            if ch == '\n' {
2143                lines.push(current);
2144                current = Vec::new();
2145                continue;
2146            }
2147
2148            if wrap {
2149                if let Some(cols) = wrap_cols {
2150                    if current.len() >= cols {
2151                        lines.push(current);
2152                        current = Vec::new();
2153                    }
2154                }
2155            }
2156
2157            current.push(ch);
2158        }
2159
2160        lines.push(current);
2161        lines
2162    }
2163
2164    /// Performs the measure text operation.
2165    pub fn measure_text(&self, text: &str, max_width: Option<usize>, wrap: bool) -> TextMetrics {
2166        if !self.enabled {
2167            return TextMetrics {
2168                width: 0,
2169                height: 0,
2170                lines: 0,
2171            };
2172        }
2173        let gw = self.font_info.glyph_w;
2174        let gh = self.font_info.glyph_h;
2175        let max_cols = max_width.map(|w| core::cmp::max(1, w / gw));
2176        let lines = self.layout_text_lines(text, wrap, max_cols);
2177
2178        let mut max_line_cols = 0usize;
2179        for line in &lines {
2180            max_line_cols = core::cmp::max(max_line_cols, line.len());
2181        }
2182
2183        TextMetrics {
2184            width: max_line_cols * gw,
2185            height: lines.len() * gh,
2186            lines: lines.len(),
2187        }
2188    }
2189
2190    /// Performs the draw text operation.
2191    pub fn draw_text(
2192        &mut self,
2193        pixel_x: usize,
2194        pixel_y: usize,
2195        text: &str,
2196        opts: TextOptions,
2197    ) -> TextMetrics {
2198        self.render_pending_console();
2199        if !self.enabled {
2200            return TextMetrics {
2201                width: 0,
2202                height: 0,
2203                lines: 0,
2204            };
2205        }
2206
2207        let gw = self.font_info.glyph_w;
2208        let gh = self.font_info.glyph_h;
2209        let max_cols = opts.max_width.map(|w| core::cmp::max(1, w / gw));
2210        let lines = self.layout_text_lines(text, opts.wrap, max_cols);
2211        let region_w = opts.max_width.unwrap_or_else(|| {
2212            let mut max_line_cols = 0usize;
2213            for line in &lines {
2214                max_line_cols = core::cmp::max(max_line_cols, line.len());
2215            }
2216            max_line_cols * gw
2217        });
2218
2219        let fg = self.pack_color(opts.fg);
2220        let bg = self.pack_color(opts.bg);
2221        let mut max_line_px = 0usize;
2222
2223        for (line_idx, line) in lines.iter().enumerate() {
2224            let line_px = line.len() * gw;
2225            max_line_px = core::cmp::max(max_line_px, line_px);
2226            let x = match opts.align {
2227                TextAlign::Left => pixel_x,
2228                TextAlign::Center => pixel_x.saturating_add(region_w.saturating_sub(line_px) / 2),
2229                TextAlign::Right => pixel_x.saturating_add(region_w.saturating_sub(line_px)),
2230            };
2231            let y = pixel_y + line_idx * gh;
2232
2233            for (col, ch) in line.iter().enumerate() {
2234                self.draw_glyph_at_pixel(x + col * gw, y, *ch, fg, bg);
2235            }
2236        }
2237
2238        TextMetrics {
2239            width: max_line_px,
2240            height: lines.len() * gh,
2241            lines: lines.len(),
2242        }
2243    }
2244
2245    /// Performs the draw strata stack operation.
2246    pub fn draw_strata_stack(
2247        &mut self,
2248        origin_x: usize,
2249        origin_y: usize,
2250        layer_w: usize,
2251        layer_h: usize,
2252    ) {
2253        self.render_pending_console();
2254        if !self.enabled || layer_w == 0 || layer_h == 0 {
2255            return;
2256        }
2257
2258        // Simple "strata" stack: each layer is slightly shifted and tinted.
2259        let palette = [
2260            RgbColor::new(0x24, 0x3B, 0x55),
2261            RgbColor::new(0x2B, 0x54, 0x77),
2262            RgbColor::new(0x2F, 0x74, 0x93),
2263            RgbColor::new(0x3A, 0x93, 0xA8),
2264            RgbColor::new(0x5F, 0xB1, 0xA1),
2265            RgbColor::new(0xA4, 0xCC, 0x94),
2266        ];
2267
2268        let dx = 6usize;
2269        let dy = 5usize;
2270        for (i, color) in palette.iter().enumerate() {
2271            let x = origin_x.saturating_add(i * dx);
2272            let y = origin_y.saturating_add(i * dy);
2273            let w = layer_w.saturating_sub(i * dx);
2274            let h = layer_h.saturating_sub(i * dy);
2275            if w < 8 || h < 8 {
2276                break;
2277            }
2278
2279            self.fill_rect(x, y, w, h, *color);
2280            self.draw_rect(x, y, w, h, RgbColor::new(0x10, 0x16, 0x20));
2281        }
2282    }
2283
2284    /// Performs the draw glyph operation.
2285    pub(crate) fn draw_glyph(&mut self, cx: usize, cy: usize, ch: char) {
2286        let glyph_index = self.glyph_index_for_char(ch);
2287        self.draw_glyph_index_at_pixel(
2288            cx * self.font_info.glyph_w,
2289            cy * self.font_info.glyph_h,
2290            glyph_index,
2291            self.fg,
2292            self.bg,
2293        );
2294    }
2295
2296    /// Performs the clear row operation.
2297    pub(crate) fn clear_row(&mut self, row: usize) {
2298        if !self.enabled {
2299            return;
2300        }
2301        self.clear_text_line_pixels(row);
2302    }
2303
2304    /// Performs the scroll operation.
2305    pub(crate) fn scroll(&mut self) {
2306        if !self.enabled {
2307            return;
2308        }
2309        self.console_row_head = (self.console_row_head + 1) % self.rows;
2310        self.console_pending_scroll = (self.console_pending_scroll + 1).min(self.rows);
2311        let bottom = ((self.console_row_head + self.rows - 1) % self.rows) * self.cols;
2312        self.console_cells[bottom..bottom + self.cols].fill(SbCell {
2313            ch: ' ',
2314            fg: self.fg,
2315            bg: self.bg,
2316        });
2317        self.console_dirty_cells[bottom..bottom + self.cols].fill(true);
2318        self.console_pending = true;
2319        self.row = self.rows - 1;
2320    }
2321
2322    /// Writes char.
2323    pub(crate) fn write_char(&mut self, c: char) {
2324        if !self.enabled {
2325            return;
2326        }
2327        let c = normalize_console_char(c);
2328
2329        self.console_pending = true;
2330
2331        // Mirror into scrollback (always, even when scrolled back) ========================================
2332        self.sb_mirror_char(c);
2333        // If the user is viewing history, suppress live rendering.
2334        if self.scroll_offset > 0 {
2335            return;
2336        }
2337        // ==============================
2338
2339        match c {
2340            '\n' => {
2341                self.col = 0;
2342                self.row += 1;
2343            }
2344            '\r' => self.col = 0,
2345            '\t' => self.col = (self.col + 4) & !3,
2346            '\u{8}' => {
2347                if self.col > 0 {
2348                    self.col -= 1;
2349                    self.queue_console_glyph(' ');
2350                }
2351            }
2352            '\0' => {}
2353            ch => {
2354                self.queue_console_glyph(ch);
2355                self.col += 1;
2356            }
2357        }
2358
2359        if self.col >= self.cols {
2360            self.col = 0;
2361            self.row += 1;
2362        }
2363
2364        if self.row >= self.rows {
2365            self.scroll();
2366        }
2367    }
2368
2369    /// Writes bytes (render only : does NOT present or draw scrollbar).
2370    /// Call `flush_display()` after a batch of writes to present.
2371    pub(crate) fn write_bytes(&mut self, s: &str) {
2372        self.begin_viewport_render();
2373        // Skip basic ANSI escape sequences to avoid rendering control garbage.
2374        let bytes = s.as_bytes();
2375        let mut i = 0;
2376        while i < bytes.len() {
2377            let b = bytes[i];
2378            if b == 0x1B {
2379                // ESC : skip CSI sequences: ESC [ ... final_byte
2380                if i + 1 < bytes.len() && bytes[i + 1] == b'[' {
2381                    i += 2;
2382                    while i < bytes.len() {
2383                        let c = bytes[i];
2384                        i += 1;
2385                        if c >= b'@' && c <= b'~' {
2386                            break;
2387                        }
2388                    }
2389                } else {
2390                    i += 1;
2391                }
2392                continue;
2393            }
2394            // Safe: ASCII bytes 0x20..=0x7E are valid single-char UTF-8.
2395            let ch = b as char;
2396            self.write_char(ch);
2397            i += 1;
2398        }
2399        if self.draw_to_back_buffer() && (self.console_pending || self.can().dirty.len != 0) {
2400            self.canm().present_pending = true;
2401        }
2402    }
2403
2404    /// Publish a pending frame; the cadence gate precedes all rasterization.
2405    pub(crate) fn flush_display(&mut self) {
2406        if !self.enabled {
2407            return;
2408        }
2409        self.request_present();
2410    }
2411
2412    fn queue_console_glyph(&mut self, ch: char) {
2413        if self.row >= self.rows || self.col >= self.cols {
2414            return;
2415        }
2416        let idx = ((self.console_row_head + self.row) % self.rows) * self.cols + self.col;
2417        self.console_cells[idx] = SbCell {
2418            ch,
2419            fg: self.fg,
2420            bg: self.bg,
2421        };
2422        self.console_dirty_cells[idx] = true;
2423        self.console_pending = true;
2424    }
2425
2426    /// Materialize pending text before a present or an explicit graphics draw.
2427    /// No allocations, and at most one pixel scroll regardless of output volume.
2428    pub(crate) fn render_pending_console(&mut self) {
2429        if !self.enabled || !self.console_pending {
2430            return;
2431        }
2432        // Clear first: drawing primitives below may call this ordering barrier.
2433        self.console_pending = false;
2434        let mouse_visible = self.cursor.mc_visible;
2435        let text_visible = self.cursor.tc_visible;
2436        if mouse_visible {
2437            self.mc_erase_hw();
2438        }
2439        if text_visible {
2440            self.text_cursor_erase_hw();
2441        }
2442        let scroll = core::mem::take(&mut self.console_pending_scroll);
2443        if scroll != 0 {
2444            self.move_text_view_pixels_up(scroll * self.font_info.glyph_h);
2445        }
2446        for row in 0..self.rows {
2447            let start = ((self.console_row_head + row) % self.rows) * self.cols;
2448            for col in 0..self.cols {
2449                let idx = start + col;
2450                if !core::mem::replace(&mut self.console_dirty_cells[idx], false) {
2451                    continue;
2452                }
2453                let cell = self.console_cells[idx];
2454                self.draw_glyph_at_pixel(
2455                    col * self.font_info.glyph_w,
2456                    row * self.font_info.glyph_h,
2457                    cell.ch,
2458                    cell.fg,
2459                    cell.bg,
2460                );
2461            }
2462        }
2463        self.draw_scrollbar_inner();
2464        // Compose overlays into the completed frame, before present copies it.
2465        if text_visible {
2466            self.text_cursor_save_hw();
2467            self.text_cursor_draw_hw();
2468        }
2469        if mouse_visible {
2470            self.mc_save_hw();
2471            self.mc_draw_hw();
2472        }
2473    }
2474
2475    // =============================================================
2476    // Scrollback buffer + scrollbar
2477    // =============================================================
2478
2479    /// Mirror a normalized character into the scrollback model.
2480    /// Called by `write_char` before any live rendering.
2481    /// Mirror a typed character into the scrollback buffer.
2482    pub(crate) fn sb_mirror_char(&mut self, c: char) {
2483        let cols = self.cols;
2484        let fg = self.fg;
2485        let bg = self.bg;
2486        let rows = self.rows;
2487        self.sb.mirror_char(c, cols, fg, bg, rows);
2488    }
2489
2490    /// Keep the scrollback buffer within MAX_SCROLLBACK + rows.
2491    #[inline]
2492    pub(crate) fn sb_trim(&mut self) {
2493        let rows = self.rows;
2494        self.sb.trim(rows);
2495    }
2496
2497    /// Draw (or refresh) the scrollbar strip on the right edge of the text area.
2498    pub(crate) fn draw_scrollbar_inner(&mut self) {
2499        if !self.enabled || self.can().width == 0 {
2500            return;
2501        }
2502        let text_h = self.text_area_height();
2503        if text_h == 0 {
2504            return;
2505        }
2506        let sb_x = self.can().width.saturating_sub(SCROLLBAR_W);
2507        let sb_w = SCROLLBAR_W;
2508        let total = self.sb.rows.len() + 1; // +1 accounts for current partial row
2509        let track_color = self.unpack_color(self.fmt.pack_rgb(0x22, 0x28, 0x38));
2510        let thumb_color = self.unpack_color(self.fmt.pack_rgb(0x58, 0x72, 0xA0));
2511        let thumb_hi_color = self.unpack_color(self.fmt.pack_rgb(0x80, 0xA0, 0xC8));
2512
2513        if total <= self.rows {
2514            // Not enough content to scroll: full-height thumb with border.
2515            self.fill_rect(sb_x, 0, sb_w, text_h, track_color);
2516            if sb_w > 2 && text_h > 2 {
2517                self.fill_rect(sb_x + 1, 1, sb_w - 2, text_h - 2, thumb_hi_color);
2518            }
2519            return;
2520        }
2521
2522        let max_offset = total.saturating_sub(self.rows);
2523        let thumb_h = ((text_h * self.rows) / total).max(6);
2524        let avail = text_h.saturating_sub(thumb_h);
2525        // offset 0 = thumb at bottom; max_offset = thumb at top
2526        let thumb_y = if self.scroll_offset == 0 || avail == 0 {
2527            avail // = text_h - thumb_h (bottom)
2528        } else {
2529            avail - (avail * self.scroll_offset / max_offset)
2530        };
2531
2532        // Draw track background
2533        self.fill_rect(sb_x, 0, sb_w, text_h, track_color);
2534        // Draw thumb
2535        if thumb_h > 0 && thumb_h < text_h {
2536            self.fill_rect(sb_x, thumb_y, sb_w, thumb_h, thumb_color);
2537        }
2538    }
2539
2540    /// Redraw the entire text area from the scrollback buffer.
2541    /// Called when scroll_offset changes.
2542    pub(crate) fn redraw_from_scrollback(&mut self) {
2543        if !self.enabled {
2544            return;
2545        }
2546        self.render_viewport_full();
2547    }
2548
2549    pub(crate) fn redraw_from_scrollback_incremental(&mut self, old_offset: usize) -> bool {
2550        if !self.enabled || self.rows == 0 {
2551            return false;
2552        }
2553        let diff = old_offset.abs_diff(self.scroll_offset);
2554        if diff == 0 || diff >= self.rows {
2555            return false;
2556        }
2557        let pixel_delta = diff.saturating_mul(self.font_info.glyph_h);
2558        if pixel_delta == 0 {
2559            return false;
2560        }
2561
2562        let (prev_draw_to_back, prev_track_dirty) = self.begin_viewport_render();
2563
2564        if self.scroll_offset > old_offset {
2565            self.move_text_view_pixels_down(pixel_delta);
2566            self.redraw_visible_rows(0, diff);
2567        } else {
2568            self.move_text_view_pixels_up(pixel_delta);
2569            self.redraw_visible_rows(self.rows.saturating_sub(diff), diff);
2570        }
2571
2572        self.finalize_live_view_state();
2573        self.draw_scrollbar_inner();
2574        self.end_viewport_render(prev_draw_to_back, prev_track_dirty);
2575        true
2576    }
2577
2578    /// Scroll the view up (backward in history) by `lines` lines.
2579    pub fn scroll_view_up(&mut self, lines: usize) {
2580        if !self.enabled {
2581            return;
2582        }
2583        let total = self.sb.rows.len() + 1;
2584        let max_off = total.saturating_sub(self.rows);
2585        self.set_scroll_offset_and_render((self.scroll_offset + lines).min(max_off));
2586    }
2587
2588    /// Scroll the view down (forward, toward live) by `lines` lines.
2589    pub fn scroll_view_down(&mut self, lines: usize) {
2590        if !self.enabled {
2591            return;
2592        }
2593        self.set_scroll_offset_and_render(self.scroll_offset.saturating_sub(lines));
2594    }
2595
2596    /// Immediately return to the live (bottom) view.
2597    pub fn scroll_to_live(&mut self) {
2598        if self.scroll_offset == 0 {
2599            return;
2600        }
2601        self.set_scroll_offset_and_render(0);
2602    }
2603
2604    /// Handle a click at pixel `(px_x, px_y)` : if it falls in the scrollbar,
2605    /// jump the view to the corresponding scroll position.
2606    pub fn scrollbar_click(&mut self, px_x: usize, px_y: usize) {
2607        if !self.enabled {
2608            return;
2609        }
2610        let sb_x = self.can().width.saturating_sub(SCROLLBAR_W);
2611        if px_x < sb_x {
2612            return;
2613        }
2614        let text_h = self.text_area_height();
2615        if text_h <= 1 {
2616            return;
2617        }
2618        let total = self.sb.rows.len() + 1;
2619        let max_off = total.saturating_sub(self.rows);
2620        if max_off == 0 {
2621            return;
2622        }
2623        // py = 0 => top = oldest = max_offset; py = text_h - 1 => bottom = 0
2624        let py = px_y.min(text_h - 1);
2625        let offset = max_off * (text_h - 1 - py) / (text_h - 1);
2626        self.set_scroll_offset_and_render(offset.min(max_off));
2627    }
2628
2629    /// Drag the scrollbar thumb to vertical pixel `px_y`.
2630    ///
2631    /// Unlike `scrollbar_click`, this only depends on Y and is intended for
2632    /// click-and-drag interactions where the pointer may slightly leave the
2633    /// scrollbar strip horizontally.
2634    pub fn scrollbar_drag_to(&mut self, px_y: usize) {
2635        if !self.enabled {
2636            return;
2637        }
2638        let text_h = self.text_area_height();
2639        if text_h <= 1 {
2640            return;
2641        }
2642        let total = self.sb.rows.len() + 1;
2643        let max_off = total.saturating_sub(self.rows);
2644        if max_off == 0 {
2645            return;
2646        }
2647        // py = 0 -> top = oldest = max_offset; py = text_h - 1 -> bottom = 0
2648        let py = px_y.min(text_h - 1);
2649        let offset = max_off * (text_h - 1 - py) / (text_h - 1);
2650        self.set_scroll_offset_and_render(offset.min(max_off));
2651    }
2652
2653    /// Returns `true` if the pixel coordinates fall within the scrollbar strip.
2654    pub fn scrollbar_hit_test(&self, px_x: usize, px_y: usize) -> bool {
2655        if !self.enabled {
2656            return false;
2657        }
2658        let sb_x = self.can().width.saturating_sub(SCROLLBAR_W);
2659        px_x >= sb_x && px_y < self.text_area_height()
2660    }
2661}
2662
2663/// Performs the normalize console char operation.
2664fn normalize_console_char(ch: char) -> char {
2665    match ch {
2666        '\n' | '\r' | '\t' | '\u{8}' => ch,
2667        c if c.is_control() => '\0',
2668        // Graceful fallback when font lacks box-drawing coverage.
2669        '\u{2500}' | '\u{2501}' | '\u{2504}' | '\u{2505}' | '\u{2013}' | '\u{2014}' => '-',
2670        '\u{2502}' | '\u{2503}' => '|',
2671        '\u{250c}' | '\u{2510}' | '\u{2514}' | '\u{2518}' | '\u{251c}' | '\u{2524}'
2672        | '\u{252c}' | '\u{2534}' | '\u{253c}' => '+',
2673        '\u{00a0}' => ' ',
2674        _ => ch,
2675    }
2676}
2677
2678impl fmt::Write for VgaWriter {
2679    /// Writes str.
2680    fn write_str(&mut self, s: &str) -> fmt::Result {
2681        if self.enabled {
2682            self.write_bytes(s);
2683        } else {
2684            crate::arch::x86_64::serial::_print(format_args!("{}", s));
2685        }
2686        Ok(())
2687    }
2688}