Skip to main content

strat9_kernel/arch/x86_64/
vga.rs

1//! Framebuffer text console (Limine framebuffer + PSF font).
2//! https://en.wikipedia.org/wiki/PC_Screen_Font
3//!
4//! Keeps the existing `vga_print!` / `vga_println!` API but renders text into
5//! the graphical framebuffer when available. Falls back to serial otherwise.
6
7use alloc::{format, string::String, vec::Vec};
8use core::{
9    fmt,
10    sync::atomic::{AtomicBool, AtomicU64, AtomicU8, Ordering},
11};
12use spin::Mutex;
13
14/// Whether framebuffer console is available.
15static VGA_AVAILABLE: AtomicBool = AtomicBool::new(false);
16static STATUS_LAST_REFRESH_TICK: AtomicU64 = AtomicU64::new(0);
17const STATUS_REFRESH_PERIOD_TICKS: u64 = 100; // 100Hz timer => 1s
18static STATUS_LAST_IP_REFRESH_TICK: AtomicU64 = AtomicU64::new(0);
19const STATUS_IP_REFRESH_PERIOD_TICKS: u64 = 3_000; // 100Hz timer => 30s
20static PRESENTED_FRAMES: AtomicU64 = AtomicU64::new(0);
21static FPS_LAST_TICK: AtomicU64 = AtomicU64::new(0);
22static FPS_LAST_FRAME_COUNT: AtomicU64 = AtomicU64::new(0);
23static FPS_ESTIMATE: AtomicU64 = AtomicU64::new(0);
24static VGA_PRESENT_REGION_COUNT: AtomicU64 = AtomicU64::new(0);
25static VGA_PRESENT_PIXEL_COUNT: AtomicU64 = AtomicU64::new(0);
26static DOUBLE_BUFFER_MODE: AtomicBool = AtomicBool::new(false);
27static UI_SCALE: AtomicU8 = AtomicU8::new(1);
28
29const FONT_PSF: &[u8] = include_bytes!("fonts/zap-ext-light20.psf");
30
31/// VGA colors mapped to RGB for text rendering.
32#[allow(dead_code)]
33#[derive(Debug, Clone, Copy, PartialEq)]
34#[repr(u8)]
35pub enum Color {
36    Black = 0x0,
37    Blue = 0x1,
38    Green = 0x2,
39    Cyan = 0x3,
40    Red = 0x4,
41    Magenta = 0x5,
42    Brown = 0x6,
43    LightGrey = 0x7,
44    DarkGrey = 0x8,
45    LightBlue = 0x9,
46    LightGreen = 0xA,
47    LightCyan = 0xB,
48    LightRed = 0xC,
49    LightMagenta = 0xD,
50    Yellow = 0xE,
51    White = 0xF,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub struct RgbColor {
56    pub r: u8,
57    pub g: u8,
58    pub b: u8,
59}
60
61impl RgbColor {
62    /// Creates a new instance.
63    pub const fn new(r: u8, g: u8, b: u8) -> Self {
64        Self { r, g, b }
65    }
66
67    pub const BLACK: Self = Self::new(0x00, 0x00, 0x00);
68    pub const WHITE: Self = Self::new(0xFF, 0xFF, 0xFF);
69    pub const RED: Self = Self::new(0xFF, 0x00, 0x00);
70    pub const GREEN: Self = Self::new(0x00, 0xFF, 0x00);
71    pub const BLUE: Self = Self::new(0x00, 0x00, 0xFF);
72    pub const CYAN: Self = Self::new(0x00, 0xFF, 0xFF);
73    pub const MAGENTA: Self = Self::new(0xFF, 0x00, 0xFF);
74    pub const YELLOW: Self = Self::new(0xFF, 0xFF, 0x00);
75    pub const LIGHT_GREY: Self = Self::new(0xAA, 0xAA, 0xAA);
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum TextAlign {
80    Left,
81    Center,
82    Right,
83}
84
85#[derive(Debug, Clone, Copy)]
86pub struct TextOptions {
87    pub fg: RgbColor,
88    pub bg: RgbColor,
89    pub align: TextAlign,
90    pub wrap: bool,
91    pub max_width: Option<usize>,
92}
93
94impl TextOptions {
95    /// Creates a new instance.
96    pub const fn new(fg: RgbColor, bg: RgbColor) -> Self {
97        Self {
98            fg,
99            bg,
100            align: TextAlign::Left,
101            wrap: false,
102            max_width: None,
103        }
104    }
105}
106
107#[derive(Debug, Clone, Copy)]
108pub struct TextMetrics {
109    pub width: usize,
110    pub height: usize,
111    pub lines: usize,
112}
113
114#[derive(Debug, Clone, Copy)]
115pub struct SpriteRgba<'a> {
116    pub width: usize,
117    pub height: usize,
118    pub pixels: &'a [u8],
119}
120
121#[derive(Debug, Clone, Copy)]
122pub struct UiTheme {
123    pub background: RgbColor,
124    pub panel_bg: RgbColor,
125    pub panel_border: RgbColor,
126    pub text: RgbColor,
127    pub accent: RgbColor,
128    pub status_bg: RgbColor,
129    pub status_text: RgbColor,
130}
131
132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133pub enum UiScale {
134    Compact = 1,
135    Normal = 2,
136    Large = 3,
137}
138
139impl UiScale {
140    /// Performs the factor operation.
141    pub const fn factor(self) -> usize {
142        self as usize
143    }
144}
145
146impl UiTheme {
147    pub const SLATE: Self = Self {
148        background: RgbColor::new(0x12, 0x16, 0x1E),
149        panel_bg: RgbColor::new(0x1A, 0x22, 0x2C),
150        panel_border: RgbColor::new(0x3D, 0x52, 0x66),
151        text: RgbColor::new(0xE2, 0xE8, 0xF0),
152        accent: RgbColor::new(0x4F, 0xB3, 0xB3),
153        status_bg: RgbColor::new(0x0E, 0x13, 0x1A),
154        status_text: RgbColor::new(0xD3, 0xDE, 0xEA),
155    };
156
157    pub const SAND: Self = Self {
158        background: RgbColor::new(0xFA, 0xF6, 0xEF),
159        panel_bg: RgbColor::new(0xF1, 0xE8, 0xD8),
160        panel_border: RgbColor::new(0xA6, 0x8F, 0x6A),
161        text: RgbColor::new(0x2B, 0x2B, 0x2B),
162        accent: RgbColor::new(0x1F, 0x7A, 0x8C),
163        status_bg: RgbColor::new(0xE6, 0xD7, 0xBF),
164        status_text: RgbColor::new(0x2B, 0x2B, 0x2B),
165    };
166
167    pub const OCEAN_STATUS: Self = Self {
168        background: RgbColor::new(0x12, 0x16, 0x1E),
169        panel_bg: RgbColor::new(0x1A, 0x22, 0x2C),
170        panel_border: RgbColor::new(0x3D, 0x52, 0x66),
171        text: RgbColor::new(0xE2, 0xE8, 0xF0),
172        accent: RgbColor::new(0x4F, 0xB3, 0xB3),
173        status_bg: RgbColor::new(0x1B, 0x4D, 0x8A),
174        status_text: RgbColor::new(0xF5, 0xFA, 0xFF),
175    };
176}
177
178#[derive(Debug, Clone)]
179struct StatusLineInfo {
180    hostname: String,
181    ip: String,
182}
183
184static STATUS_LINE_INFO: Mutex<Option<StatusLineInfo>> = Mutex::new(None);
185
186#[derive(Debug, Clone, Copy, Default)]
187pub struct UiRect {
188    pub x: usize,
189    pub y: usize,
190    pub w: usize,
191    pub h: usize,
192}
193
194impl UiRect {
195    /// Creates a new instance.
196    pub const fn new(x: usize, y: usize, w: usize, h: usize) -> Self {
197        Self { x, y, w, h }
198    }
199}
200
201#[derive(Debug, Clone, Copy)]
202pub enum DockEdge {
203    Top,
204    Bottom,
205    Left,
206    Right,
207}
208
209#[derive(Debug, Clone, Copy)]
210pub struct UiDockLayout {
211    remaining: UiRect,
212}
213
214impl UiDockLayout {
215    /// Builds this from screen.
216    pub fn from_screen() -> Self {
217        Self {
218            remaining: UiRect::new(0, 0, width(), height()),
219        }
220    }
221
222    /// Builds this from rect.
223    pub const fn from_rect(rect: UiRect) -> Self {
224        Self { remaining: rect }
225    }
226
227    /// Performs the remaining operation.
228    pub const fn remaining(&self) -> UiRect {
229        self.remaining
230    }
231
232    /// Performs the dock operation.
233    pub fn dock(&mut self, edge: DockEdge, size: usize) -> UiRect {
234        match edge {
235            DockEdge::Top => {
236                let h = core::cmp::min(size, self.remaining.h);
237                let out = UiRect::new(self.remaining.x, self.remaining.y, self.remaining.w, h);
238                self.remaining.y = self.remaining.y.saturating_add(h);
239                self.remaining.h = self.remaining.h.saturating_sub(h);
240                out
241            }
242            DockEdge::Bottom => {
243                let h = core::cmp::min(size, self.remaining.h);
244                let y = self
245                    .remaining
246                    .y
247                    .saturating_add(self.remaining.h.saturating_sub(h));
248                let out = UiRect::new(self.remaining.x, y, self.remaining.w, h);
249                self.remaining.h = self.remaining.h.saturating_sub(h);
250                out
251            }
252            DockEdge::Left => {
253                let w = core::cmp::min(size, self.remaining.w);
254                let out = UiRect::new(self.remaining.x, self.remaining.y, w, self.remaining.h);
255                self.remaining.x = self.remaining.x.saturating_add(w);
256                self.remaining.w = self.remaining.w.saturating_sub(w);
257                out
258            }
259            DockEdge::Right => {
260                let w = core::cmp::min(size, self.remaining.w);
261                let x = self
262                    .remaining
263                    .x
264                    .saturating_add(self.remaining.w.saturating_sub(w));
265                let out = UiRect::new(x, self.remaining.y, w, self.remaining.h);
266                self.remaining.w = self.remaining.w.saturating_sub(w);
267                out
268            }
269        }
270    }
271}
272
273#[derive(Debug, Clone)]
274pub struct UiLabel<'a> {
275    pub rect: UiRect,
276    pub text: &'a str,
277    pub fg: RgbColor,
278    pub bg: RgbColor,
279    pub align: TextAlign,
280}
281
282#[derive(Debug, Clone)]
283pub struct UiPanel<'a> {
284    pub rect: UiRect,
285    pub title: &'a str,
286    pub body: &'a str,
287    pub theme: UiTheme,
288}
289
290#[derive(Debug, Clone, Copy)]
291pub struct UiProgressBar {
292    pub rect: UiRect,
293    pub value: u8, // 0..=100
294    pub fg: RgbColor,
295    pub bg: RgbColor,
296    pub border: RgbColor,
297}
298
299#[derive(Debug, Clone)]
300pub struct UiTable {
301    pub rect: UiRect,
302    pub headers: Vec<String>,
303    pub rows: Vec<Vec<String>>,
304    pub theme: UiTheme,
305}
306
307#[derive(Debug, Clone)]
308struct TerminalLine {
309    text: String,
310    fg: RgbColor,
311}
312
313#[derive(Debug, Clone)]
314pub struct TerminalWidget {
315    pub rect: UiRect,
316    pub title: String,
317    pub fg: RgbColor,
318    pub bg: RgbColor,
319    pub border: RgbColor,
320    pub max_lines: usize,
321    lines: Vec<TerminalLine>,
322}
323
324impl TerminalWidget {
325    /// Creates a new instance.
326    pub fn new(rect: UiRect, max_lines: usize) -> Self {
327        Self {
328            rect,
329            title: String::from("Terminal"),
330            fg: RgbColor::LIGHT_GREY,
331            bg: RgbColor::new(0x0F, 0x14, 0x1B),
332            border: RgbColor::new(0x3D, 0x52, 0x66),
333            max_lines: core::cmp::max(1, max_lines),
334            lines: Vec::new(),
335        }
336    }
337
338    /// Performs the push line operation.
339    pub fn push_line(&mut self, text: &str) {
340        self.push_colored_line(text, self.fg);
341    }
342
343    /// Performs the push ansi line operation.
344    pub fn push_ansi_line(&mut self, text: &str) {
345        let (fg, stripped) = parse_ansi_color_prefix(text, self.fg);
346        self.push_colored_line(&stripped, fg);
347    }
348
349    /// Performs the push colored line operation.
350    fn push_colored_line(&mut self, text: &str, fg: RgbColor) {
351        if self.lines.len() >= self.max_lines {
352            self.lines.remove(0);
353        }
354        self.lines.push(TerminalLine {
355            text: String::from(text),
356            fg,
357        });
358    }
359
360    /// Performs the clear operation.
361    pub fn clear(&mut self) {
362        self.lines.clear();
363    }
364
365    /// Performs the draw operation.
366    pub fn draw(&self) {
367        let _ = with_writer(|w| {
368            if self.rect.w < 8 || self.rect.h < 8 {
369                return;
370            }
371            let (gw, gh) = w.glyph_size();
372            if gw == 0 || gh == 0 {
373                return;
374            }
375
376            w.fill_rect(self.rect.x, self.rect.y, self.rect.w, self.rect.h, self.bg);
377            w.draw_rect(
378                self.rect.x,
379                self.rect.y,
380                self.rect.w,
381                self.rect.h,
382                self.border,
383            );
384
385            let title_h = gh + 2;
386            w.fill_rect(
387                self.rect.x + 1,
388                self.rect.y + 1,
389                self.rect.w.saturating_sub(2),
390                title_h,
391                self.border,
392            );
393            w.draw_text(
394                self.rect.x + 4,
395                self.rect.y + 1,
396                &self.title,
397                TextOptions {
398                    fg: RgbColor::WHITE,
399                    bg: self.border,
400                    align: TextAlign::Left,
401                    wrap: false,
402                    max_width: Some(self.rect.w.saturating_sub(8)),
403                },
404            );
405
406            let content_y = self.rect.y + title_h + 2;
407            let content_h = self.rect.h.saturating_sub(title_h + 3);
408            let rows = core::cmp::max(1, content_h / gh);
409            let start = self.lines.len().saturating_sub(rows);
410
411            for (idx, line) in self.lines.iter().skip(start).enumerate() {
412                let y = content_y + idx * gh;
413                w.draw_text(
414                    self.rect.x + 4,
415                    y,
416                    &line.text,
417                    TextOptions {
418                        fg: line.fg,
419                        bg: self.bg,
420                        align: TextAlign::Left,
421                        wrap: false,
422                        max_width: Some(self.rect.w.saturating_sub(8)),
423                    },
424                );
425            }
426        });
427    }
428}
429
430/// Parses ansi color prefix.
431fn parse_ansi_color_prefix(input: &str, default_fg: RgbColor) -> (RgbColor, String) {
432    let bytes = input.as_bytes();
433    if !bytes.starts_with(b"\x1b[") {
434        return (default_fg, String::from(input));
435    }
436    let Some(mpos) = bytes.iter().position(|b| *b == b'm') else {
437        return (default_fg, String::from(input));
438    };
439    let code = &input[2..mpos];
440    let rest = &input[mpos + 1..];
441    let fg = match code {
442        "30" => RgbColor::BLACK,
443        "31" => RgbColor::new(0xFF, 0x55, 0x55),
444        "32" => RgbColor::new(0x66, 0xFF, 0x66),
445        "33" => RgbColor::new(0xFF, 0xDD, 0x66),
446        "34" => RgbColor::new(0x77, 0xAA, 0xFF),
447        "35" => RgbColor::new(0xFF, 0x77, 0xFF),
448        "36" => RgbColor::new(0x77, 0xFF, 0xFF),
449        "37" | "0" => RgbColor::LIGHT_GREY,
450        _ => default_fg,
451    };
452    (fg, String::from(rest))
453}
454
455/// Performs the color to rgb operation.
456#[inline]
457fn color_to_rgb(c: Color) -> (u8, u8, u8) {
458    match c {
459        Color::Black => (0x00, 0x00, 0x00),
460        Color::Blue => (0x00, 0x00, 0xAA),
461        Color::Green => (0x00, 0xAA, 0x00),
462        Color::Cyan => (0x00, 0xAA, 0xAA),
463        Color::Red => (0xAA, 0x00, 0x00),
464        Color::Magenta => (0xAA, 0x00, 0xAA),
465        Color::Brown => (0xAA, 0x55, 0x00),
466        Color::LightGrey => (0xAA, 0xAA, 0xAA),
467        Color::DarkGrey => (0x55, 0x55, 0x55),
468        Color::LightBlue => (0x55, 0x55, 0xFF),
469        Color::LightGreen => (0x55, 0xFF, 0x55),
470        Color::LightCyan => (0x55, 0xFF, 0xFF),
471        Color::LightRed => (0xFF, 0x55, 0x55),
472        Color::LightMagenta => (0xFF, 0x55, 0xFF),
473        Color::Yellow => (0xFF, 0xFF, 0x55),
474        Color::White => (0xFF, 0xFF, 0xFF),
475    }
476}
477
478impl From<Color> for RgbColor {
479    /// Performs the from operation.
480    fn from(value: Color) -> Self {
481        let (r, g, b) = color_to_rgb(value);
482        Self::new(r, g, b)
483    }
484}
485
486#[derive(Clone, Copy)]
487struct PixelFormat {
488    bpp: u16,
489    red_size: u8,
490    red_shift: u8,
491    green_size: u8,
492    green_shift: u8,
493    blue_size: u8,
494    blue_shift: u8,
495}
496
497#[derive(Debug, Clone, Copy)]
498pub struct FramebufferInfo {
499    pub available: bool,
500    pub width: usize,
501    pub height: usize,
502    pub pitch: usize,
503    pub bpp: u16,
504    pub red_size: u8,
505    pub red_shift: u8,
506    pub green_size: u8,
507    pub green_shift: u8,
508    pub blue_size: u8,
509    pub blue_shift: u8,
510    pub text_cols: usize,
511    pub text_rows: usize,
512    pub glyph_w: usize,
513    pub glyph_h: usize,
514    pub double_buffer_mode: bool,
515    pub double_buffer_enabled: bool,
516    pub ui_scale: UiScale,
517}
518
519#[derive(Debug, Clone, Copy)]
520pub struct RenderStats {
521    pub presented_frames: u64,
522    pub estimated_fps: u64,
523    pub present_region_count: u64,
524    pub present_pixel_count: u64,
525}
526
527impl PixelFormat {
528    /// Performs the pack rgb operation.
529    fn pack_rgb(&self, r: u8, g: u8, b: u8) -> u32 {
530        /// Performs the scale operation.
531        fn scale(v: u8, bits: u8) -> u32 {
532            if bits == 0 {
533                0
534            } else if bits >= 8 {
535                (v as u32) << (bits - 8)
536            } else {
537                (v as u32) >> (8 - bits)
538            }
539        }
540
541        (scale(r, self.red_size) << self.red_shift)
542            | (scale(g, self.green_size) << self.green_shift)
543            | (scale(b, self.blue_size) << self.blue_shift)
544    }
545}
546
547struct FontInfo {
548    glyph_count: usize,
549    bytes_per_glyph: usize,
550    glyph_w: usize,
551    glyph_h: usize,
552    data_offset: usize,
553    unicode_table_offset: Option<usize>,
554}
555
556#[derive(Clone, Copy)]
557struct ClipRect {
558    x: usize,
559    y: usize,
560    w: usize,
561    h: usize,
562}
563
564#[derive(Clone, Copy)]
565struct DirtyRegionSet {
566    rects: [ClipRect; MAX_DIRTY_RECTS],
567    len: usize,
568}
569
570impl DirtyRegionSet {
571    const fn empty() -> Self {
572        Self {
573            rects: [ClipRect {
574                x: 0,
575                y: 0,
576                w: 0,
577                h: 0,
578            }; MAX_DIRTY_RECTS],
579            len: 0,
580        }
581    }
582}
583
584/// Parses psf.
585fn parse_psf(font: &[u8]) -> Option<FontInfo> {
586    // PSF1
587    if font.len() >= 4 && font[0] == 0x36 && font[1] == 0x04 {
588        let mode = font[2];
589        let glyph_count = if (mode & 0x01) != 0 { 512 } else { 256 };
590        let glyph_h = font[3] as usize;
591        let bytes_per_glyph = glyph_h;
592        return Some(FontInfo {
593            glyph_count,
594            bytes_per_glyph,
595            glyph_w: 8,
596            glyph_h,
597            data_offset: 4,
598            unicode_table_offset: None,
599        });
600    }
601
602    // PSF2
603    if font.len() >= 32 && font[0] == 0x72 && font[1] == 0xB5 && font[2] == 0x4A && font[3] == 0x86
604    {
605        let rd_u32 = |off: usize| -> u32 {
606            u32::from_le_bytes([font[off], font[off + 1], font[off + 2], font[off + 3]])
607        };
608        let headersize = rd_u32(8) as usize;
609        let flags = rd_u32(12);
610        let glyph_count = rd_u32(16) as usize;
611        let bytes_per_glyph = rd_u32(20) as usize;
612        let glyph_h = rd_u32(24) as usize;
613        let glyph_w = rd_u32(28) as usize;
614        let glyph_bytes = glyph_count.saturating_mul(bytes_per_glyph);
615        let unicode_table_offset = if (flags & 1) != 0 {
616            Some(headersize.saturating_add(glyph_bytes))
617        } else {
618            None
619        };
620        return Some(FontInfo {
621            glyph_count,
622            bytes_per_glyph,
623            glyph_w,
624            glyph_h,
625            data_offset: headersize,
626            unicode_table_offset,
627        });
628    }
629
630    None
631}
632
633/// Performs the decode utf8 at operation.
634fn decode_utf8_at(bytes: &[u8], pos: usize) -> Option<(u32, usize)> {
635    let b0 = *bytes.get(pos)?;
636    if b0 < 0x80 {
637        return Some((b0 as u32, 1));
638    }
639    if (b0 & 0xE0) == 0xC0 {
640        let b1 = *bytes.get(pos + 1)?;
641        if (b1 & 0xC0) != 0x80 {
642            return None;
643        }
644        let cp = (((b0 & 0x1F) as u32) << 6) | ((b1 & 0x3F) as u32);
645        return Some((cp, 2));
646    }
647    if (b0 & 0xF0) == 0xE0 {
648        let b1 = *bytes.get(pos + 1)?;
649        let b2 = *bytes.get(pos + 2)?;
650        if (b1 & 0xC0) != 0x80 || (b2 & 0xC0) != 0x80 {
651            return None;
652        }
653        let cp = (((b0 & 0x0F) as u32) << 12) | (((b1 & 0x3F) as u32) << 6) | ((b2 & 0x3F) as u32);
654        return Some((cp, 3));
655    }
656    if (b0 & 0xF8) == 0xF0 {
657        let b1 = *bytes.get(pos + 1)?;
658        let b2 = *bytes.get(pos + 2)?;
659        let b3 = *bytes.get(pos + 3)?;
660        if (b1 & 0xC0) != 0x80 || (b2 & 0xC0) != 0x80 || (b3 & 0xC0) != 0x80 {
661            return None;
662        }
663        let cp = (((b0 & 0x07) as u32) << 18)
664            | (((b1 & 0x3F) as u32) << 12)
665            | (((b2 & 0x3F) as u32) << 6)
666            | ((b3 & 0x3F) as u32);
667        return Some((cp, 4));
668    }
669    None
670}
671
672/// Parses psf2 unicode map.
673fn parse_psf2_unicode_map(font: &[u8], info: &FontInfo) -> Vec<(u32, usize)> {
674    let Some(mut i) = info.unicode_table_offset else {
675        return Vec::new();
676    };
677    if i >= font.len() {
678        return Vec::new();
679    }
680
681    let mut map = Vec::new();
682    for glyph in 0..info.glyph_count {
683        while i < font.len() {
684            let b = font[i];
685            if b == 0xFF {
686                i += 1;
687                break;
688            }
689            if b == 0xFE {
690                // PSF2 sequence marker; skip marker and continue parsing bytes until glyph separator.
691                i += 1;
692                continue;
693            }
694            if let Some((cp, adv)) = decode_utf8_at(font, i) {
695                if !map.iter().any(|(u, _)| *u == cp) {
696                    map.push((cp, glyph));
697                }
698                i += adv;
699            } else {
700                i += 1;
701            }
702        }
703    }
704
705    map
706}
707
708/// Width of the scrollbar strip on the right edge of the text console.
709const SCROLLBAR_W: usize = 12;
710/// Maximum number of completed lines kept in the scrollback history.
711const MAX_SCROLLBACK: usize = 500;
712const MAX_DIRTY_RECTS: usize = 8;
713
714// Mouse cursor (X arrow, 12×16) ==================================================================================================================================
715const CURSOR_W: usize = 12;
716const CURSOR_H: usize = 16;
717const TEXT_CURSOR_MAX_DIM: usize = 32;
718const PRESENT_MIN_TICKS: u64 = 1;
719// 0=transparent, 1=black outline, 2=white fill
720#[rustfmt::skip]
721const CURSOR_PIXELS: [u8; CURSOR_W * CURSOR_H] = [
722    1,0,0,0,0,0,0,0,0,0,0,0,
723    1,1,0,0,0,0,0,0,0,0,0,0,
724    1,2,1,0,0,0,0,0,0,0,0,0,
725    1,2,2,1,0,0,0,0,0,0,0,0,
726    1,2,2,2,1,0,0,0,0,0,0,0,
727    1,2,2,2,2,1,0,0,0,0,0,0,
728    1,2,2,2,2,2,1,0,0,0,0,0,
729    1,2,2,2,2,2,2,1,0,0,0,0,
730    1,2,2,2,2,2,2,2,1,0,0,0,
731    1,2,2,2,2,2,1,1,0,0,0,0,
732    1,2,2,2,1,0,0,0,0,0,0,0,
733    1,2,1,1,2,2,1,0,0,0,0,0,
734    1,1,0,0,1,2,2,1,0,0,0,0,
735    0,0,0,0,0,1,2,1,0,0,0,0,
736    0,0,0,0,0,0,1,1,0,0,0,0,
737    0,0,0,0,0,0,0,0,0,0,0,0,
738];
739
740// Selection clipboard ==========================================================================================================================================================================
741const CLIPBOARD_CAP: usize = 8192;
742static CLIPBOARD: spin::Mutex<([u8; CLIPBOARD_CAP], usize)> =
743    spin::Mutex::new(([0u8; CLIPBOARD_CAP], 0));
744
745/// A single character cell stored in the scrollback buffer.
746#[derive(Clone, Copy)]
747struct SbCell {
748    ch: char,
749    fg: u32, // packed pixel color
750    bg: u32, // packed pixel color
751}
752
753pub struct VgaWriter {
754    enabled: bool,
755    fb_addr: *mut u8,
756    fb_width: usize,
757    fb_height: usize,
758    pitch: usize,
759    fmt: PixelFormat,
760
761    cols: usize,
762    rows: usize,
763    col: usize,
764    row: usize,
765
766    fg: u32,
767    bg: u32,
768
769    font: &'static [u8],
770    font_info: FontInfo,
771    glyph_mask_cache: Vec<u8>,
772    unicode_map: Vec<(u32, usize)>,
773    status_bar_height: usize,
774    clip: ClipRect,
775    back_buffer: Option<Vec<u32>>,
776    draw_to_back: bool,
777    dirty_regions: DirtyRegionSet,
778    track_dirty: bool,
779
780    // Scrollback buffer ==========================================================================================================================================================================
781    /// Completed screen rows kept for history scrolling.
782    sb_rows: Vec<Vec<SbCell>>,
783    /// Current (incomplete) row being assembled by write_char.
784    sb_cur_row: Vec<SbCell>,
785    sb_row_head: usize,
786    /// Lines scrolled back from the bottom (0 = live view).
787    scroll_offset: usize,
788
789    // Mouse cursor ====================
790    mc_x: i32,
791    mc_y: i32,
792    mc_visible: bool,
793    mc_save: [u32; CURSOR_W * CURSOR_H],
794    tc_col: usize,
795    tc_row: usize,
796    tc_w: usize,
797    tc_h: usize,
798    tc_visible: bool,
799    tc_color: u32,
800    tc_save: [u32; TEXT_CURSOR_MAX_DIM * TEXT_CURSOR_MAX_DIM],
801
802    // Text selection ==========
803    sel_active: bool,
804    sel_start_row: usize,
805    sel_start_col: usize,
806    sel_end_row: usize,
807    sel_end_col: usize,
808    present_pending: bool,
809    last_present_tick: u64,
810    prepared_row_cells: Vec<(usize, u32, u32)>,
811}
812
813unsafe impl Send for VgaWriter {}
814
815impl VgaWriter {
816    fn sb_capacity(&self) -> usize {
817        MAX_SCROLLBACK + self.rows + 1
818    }
819
820    fn sb_row_at(&self, logical_idx: usize) -> Option<&Vec<SbCell>> {
821        if logical_idx >= self.sb_rows.len() {
822            return None;
823        }
824        let phys = (self.sb_row_head + logical_idx) % self.sb_rows.len();
825        self.sb_rows.get(phys)
826    }
827
828    fn sb_push_row(&mut self, row: Vec<SbCell>) {
829        let cap = self.sb_capacity();
830        if cap == 0 {
831            return;
832        }
833        if self.sb_rows.len() < cap {
834            self.sb_rows.push(row);
835            return;
836        }
837        if self.sb_rows.is_empty() {
838            self.sb_rows.push(row);
839            self.sb_row_head = 0;
840            return;
841        }
842        self.sb_rows[self.sb_row_head] = row;
843        self.sb_row_head = (self.sb_row_head + 1) % self.sb_rows.len();
844    }
845
846    fn build_glyph_mask_cache(font: &'static [u8], info: &FontInfo) -> Vec<u8> {
847        let glyph_pixels = info.glyph_w.saturating_mul(info.glyph_h);
848        let total_pixels = info.glyph_count.saturating_mul(glyph_pixels);
849        let mut cache = Vec::with_capacity(total_pixels);
850        if glyph_pixels == 0 || info.glyph_count == 0 {
851            return cache;
852        }
853
854        let row_bytes = info.glyph_w.div_ceil(8);
855        for glyph_index in 0..info.glyph_count {
856            let start = info.data_offset + glyph_index * info.bytes_per_glyph;
857            let end = start.saturating_add(info.bytes_per_glyph);
858            if end > font.len() {
859                cache.resize(cache.len().saturating_add(glyph_pixels), 0);
860                continue;
861            }
862            let glyph_ptr = font[start..end].as_ptr();
863            for gy in 0..info.glyph_h {
864                for gx in 0..info.glyph_w {
865                    let byte = unsafe { *glyph_ptr.add(gy * row_bytes + gx / 8) };
866                    let mask = 0x80u8 >> (gx % 8);
867                    cache.push(if (byte & mask) != 0 { 1 } else { 0 });
868                }
869            }
870        }
871        cache
872    }
873
874    fn glyph_mask_slice(&self, glyph_index: usize) -> Option<&[u8]> {
875        let glyph_pixels = self
876            .font_info
877            .glyph_w
878            .saturating_mul(self.font_info.glyph_h);
879        if glyph_pixels == 0 {
880            return None;
881        }
882        let start = glyph_index.checked_mul(glyph_pixels)?;
883        let end = start.checked_add(glyph_pixels)?;
884        self.glyph_mask_cache.get(start..end)
885    }
886
887    /// Creates a new instance.
888    pub const fn new() -> Self {
889        Self {
890            enabled: false,
891            fb_addr: core::ptr::null_mut(),
892            fb_width: 0,
893            fb_height: 0,
894            pitch: 0,
895            fmt: PixelFormat {
896                bpp: 0,
897                red_size: 0,
898                red_shift: 0,
899                green_size: 0,
900                green_shift: 0,
901                blue_size: 0,
902                blue_shift: 0,
903            },
904            cols: 0,
905            rows: 0,
906            col: 0,
907            row: 0,
908            fg: 0,
909            bg: 0,
910            font: &[],
911            font_info: FontInfo {
912                glyph_count: 0,
913                bytes_per_glyph: 0,
914                glyph_w: 8,
915                glyph_h: 16,
916                data_offset: 0,
917                unicode_table_offset: None,
918            },
919            glyph_mask_cache: Vec::new(),
920            unicode_map: Vec::new(),
921            status_bar_height: 0,
922            clip: ClipRect {
923                x: 0,
924                y: 0,
925                w: 0,
926                h: 0,
927            },
928            back_buffer: None,
929            draw_to_back: false,
930            dirty_regions: DirtyRegionSet::empty(),
931            track_dirty: false,
932            sb_rows: Vec::new(),
933            sb_cur_row: Vec::new(),
934            sb_row_head: 0,
935            scroll_offset: 0,
936            mc_x: 0,
937            mc_y: 0,
938            mc_visible: false,
939            mc_save: [0u32; CURSOR_W * CURSOR_H],
940            tc_col: 0,
941            tc_row: 0,
942            tc_w: 0,
943            tc_h: 0,
944            tc_visible: false,
945            tc_color: 0,
946            tc_save: [0u32; TEXT_CURSOR_MAX_DIM * TEXT_CURSOR_MAX_DIM],
947            sel_active: false,
948            sel_start_row: 0,
949            sel_start_col: 0,
950            sel_end_row: 0,
951            sel_end_col: 0,
952            present_pending: false,
953            last_present_tick: 0,
954            prepared_row_cells: Vec::new(),
955        }
956    }
957
958    /// Performs the configure operation.
959    fn configure(
960        &mut self,
961        fb_addr: *mut u8,
962        fb_width: usize,
963        fb_height: usize,
964        pitch: usize,
965        fmt: PixelFormat,
966    ) -> bool {
967        let Some(font_info) = parse_psf(FONT_PSF) else {
968            return false;
969        };
970        let status_bar_height = font_info.glyph_h;
971        let text_height = fb_height.saturating_sub(status_bar_height);
972        let cols = fb_width / font_info.glyph_w;
973        let rows = text_height / font_info.glyph_h;
974        if cols == 0 || rows == 0 {
975            return false;
976        }
977        let (fr, fg, fb) = color_to_rgb(Color::LightGrey);
978        let (br, bg, bb) = color_to_rgb(Color::Black);
979
980        self.enabled = true;
981        self.fb_addr = fb_addr;
982        self.fb_width = fb_width;
983        self.fb_height = fb_height;
984        self.pitch = pitch;
985        self.fmt = fmt;
986        self.cols = cols;
987        self.rows = rows;
988        self.col = 0;
989        self.row = 0;
990        self.fg = fmt.pack_rgb(fr, fg, fb);
991        self.bg = fmt.pack_rgb(br, bg, bb);
992        self.font = FONT_PSF;
993        self.font_info = font_info;
994        self.glyph_mask_cache = Self::build_glyph_mask_cache(FONT_PSF, &self.font_info);
995        self.unicode_map = parse_psf2_unicode_map(FONT_PSF, &self.font_info);
996        self.status_bar_height = status_bar_height;
997        self.clip = ClipRect {
998            x: 0,
999            y: 0,
1000            w: fb_width,
1001            h: fb_height,
1002        };
1003        self.back_buffer = None;
1004        self.draw_to_back = false;
1005        self.dirty_regions = DirtyRegionSet::empty();
1006        self.track_dirty = false;
1007        self.sb_rows = Vec::new();
1008        self.sb_cur_row = Vec::new();
1009        self.sb_row_head = 0;
1010        self.scroll_offset = 0;
1011        self.mc_visible = false;
1012        self.tc_visible = false;
1013        self.tc_col = 0;
1014        self.tc_row = 0;
1015        self.tc_w = 0;
1016        self.tc_h = 0;
1017        self.tc_color = 0;
1018        self.sel_active = false;
1019        self.present_pending = false;
1020        self.last_present_tick = 0;
1021        self.prepared_row_cells = Vec::new();
1022        true
1023    }
1024
1025    /// Performs the pack color operation.
1026    #[inline]
1027    fn pack_color(&self, color: RgbColor) -> u32 {
1028        self.fmt.pack_rgb(color.r, color.g, color.b)
1029    }
1030
1031    /// Performs the unpack color operation.
1032    fn unpack_color(&self, value: u32) -> RgbColor {
1033        /// Performs the unscale operation.
1034        fn unscale(v: u32, bits: u8) -> u8 {
1035            if bits == 0 {
1036                return 0;
1037            }
1038            let max = (1u32 << bits) - 1;
1039            ((v * 255) / max) as u8
1040        }
1041
1042        let r = unscale(
1043            (value >> self.fmt.red_shift) & ((1u32 << self.fmt.red_size) - 1),
1044            self.fmt.red_size,
1045        );
1046        let g = unscale(
1047            (value >> self.fmt.green_shift) & ((1u32 << self.fmt.green_size) - 1),
1048            self.fmt.green_size,
1049        );
1050        let b = unscale(
1051            (value >> self.fmt.blue_shift) & ((1u32 << self.fmt.blue_size) - 1),
1052            self.fmt.blue_size,
1053        );
1054        RgbColor::new(r, g, b)
1055    }
1056
1057    /// Sets color.
1058    pub fn set_color(&mut self, fg: Color, bg: Color) {
1059        self.set_rgb_color(fg.into(), bg.into());
1060    }
1061
1062    /// Sets rgb color.
1063    pub fn set_rgb_color(&mut self, fg: RgbColor, bg: RgbColor) {
1064        self.fg = self.pack_color(fg);
1065        self.bg = self.pack_color(bg);
1066    }
1067
1068    /// Performs the text colors operation.
1069    pub fn text_colors(&self) -> (RgbColor, RgbColor) {
1070        (self.unpack_color(self.fg), self.unpack_color(self.bg))
1071    }
1072
1073    /// Performs the width operation.
1074    pub fn width(&self) -> usize {
1075        self.fb_width
1076    }
1077
1078    /// Performs the height operation.
1079    pub fn height(&self) -> usize {
1080        self.fb_height
1081    }
1082
1083    /// Performs the cols operation.
1084    pub fn cols(&self) -> usize {
1085        self.cols
1086    }
1087
1088    /// Performs the rows operation.
1089    pub fn rows(&self) -> usize {
1090        self.rows
1091    }
1092
1093    /// Performs the glyph size operation.
1094    pub fn glyph_size(&self) -> (usize, usize) {
1095        (self.font_info.glyph_w, self.font_info.glyph_h)
1096    }
1097
1098    /// Sets cursor cell.
1099    pub fn set_cursor_cell(&mut self, col: usize, row: usize) {
1100        if !self.enabled || self.cols == 0 || self.rows == 0 {
1101            return;
1102        }
1103        if self.tc_visible {
1104            self.text_cursor_erase_hw();
1105            self.tc_visible = false;
1106        }
1107        self.col = core::cmp::min(col, self.cols - 1);
1108        self.row = core::cmp::min(row, self.rows - 1);
1109    }
1110
1111    /// Performs the text area height operation.
1112    fn text_area_height(&self) -> usize {
1113        self.fb_height.saturating_sub(self.status_bar_height)
1114    }
1115
1116    /// Performs the enabled operation.
1117    pub fn enabled(&self) -> bool {
1118        self.enabled
1119    }
1120
1121    /// Performs the framebuffer info operation.
1122    pub fn framebuffer_info(&self) -> FramebufferInfo {
1123        FramebufferInfo {
1124            available: self.enabled,
1125            width: self.fb_width,
1126            height: self.fb_height,
1127            pitch: self.pitch,
1128            bpp: self.fmt.bpp,
1129            red_size: self.fmt.red_size,
1130            red_shift: self.fmt.red_shift,
1131            green_size: self.fmt.green_size,
1132            green_shift: self.fmt.green_shift,
1133            blue_size: self.fmt.blue_size,
1134            blue_shift: self.fmt.blue_shift,
1135            text_cols: self.cols,
1136            text_rows: self.rows,
1137            glyph_w: self.font_info.glyph_w,
1138            glyph_h: self.font_info.glyph_h,
1139            double_buffer_mode: DOUBLE_BUFFER_MODE.load(Ordering::Relaxed),
1140            double_buffer_enabled: self.draw_to_back && self.back_buffer.is_some(),
1141            ui_scale: current_ui_scale(),
1142        }
1143    }
1144
1145    /// Performs the in clip operation.
1146    #[inline]
1147    fn in_clip(&self, x: usize, y: usize) -> bool {
1148        x >= self.clip.x
1149            && y >= self.clip.y
1150            && x < self.clip.x.saturating_add(self.clip.w)
1151            && y < self.clip.y.saturating_add(self.clip.h)
1152    }
1153
1154    /// Performs the clipped rect operation.
1155    fn clipped_rect(
1156        &self,
1157        x: usize,
1158        y: usize,
1159        width: usize,
1160        height: usize,
1161    ) -> Option<(usize, usize, usize, usize)> {
1162        if width == 0 || height == 0 || !self.enabled {
1163            return None;
1164        }
1165        let src_x2 = core::cmp::min(x.saturating_add(width), self.fb_width);
1166        let src_y2 = core::cmp::min(y.saturating_add(height), self.fb_height);
1167        let clip_x2 = self.clip.x.saturating_add(self.clip.w);
1168        let clip_y2 = self.clip.y.saturating_add(self.clip.h);
1169
1170        let sx = core::cmp::max(x, self.clip.x);
1171        let sy = core::cmp::max(y, self.clip.y);
1172        let ex = core::cmp::min(src_x2, clip_x2);
1173        let ey = core::cmp::min(src_y2, clip_y2);
1174        if ex <= sx || ey <= sy {
1175            return None;
1176        }
1177        Some((sx, sy, ex - sx, ey - sy))
1178    }
1179
1180    /// Performs the clear dirty operation.
1181    fn clear_dirty(&mut self) {
1182        self.dirty_regions = DirtyRegionSet::empty();
1183    }
1184
1185    /// Performs the mark dirty rect operation.
1186    fn mark_dirty_rect(&mut self, x: usize, y: usize, width: usize, height: usize) {
1187        if !self.track_dirty {
1188            return;
1189        }
1190        let Some((sx, sy, sw, sh)) = self.clipped_rect(x, y, width, height) else {
1191            return;
1192        };
1193        let next = ClipRect {
1194            x: sx,
1195            y: sy,
1196            w: sw,
1197            h: sh,
1198        };
1199        let mut merged = next;
1200        let mut idx = 0usize;
1201        while idx < self.dirty_regions.len {
1202            let cur = self.dirty_regions.rects[idx];
1203            let overlaps = merged.x <= cur.x.saturating_add(cur.w)
1204                && merged.x.saturating_add(merged.w) >= cur.x
1205                && merged.y <= cur.y.saturating_add(cur.h)
1206                && merged.y.saturating_add(merged.h) >= cur.y;
1207            if overlaps {
1208                let x0 = core::cmp::min(cur.x, merged.x);
1209                let y0 = core::cmp::min(cur.y, merged.y);
1210                let x1 = core::cmp::max(
1211                    cur.x.saturating_add(cur.w),
1212                    merged.x.saturating_add(merged.w),
1213                );
1214                let y1 = core::cmp::max(
1215                    cur.y.saturating_add(cur.h),
1216                    merged.y.saturating_add(merged.h),
1217                );
1218                merged = ClipRect {
1219                    x: x0,
1220                    y: y0,
1221                    w: x1.saturating_sub(x0),
1222                    h: y1.saturating_sub(y0),
1223                };
1224                self.dirty_regions.rects[idx] =
1225                    self.dirty_regions.rects[self.dirty_regions.len - 1];
1226                self.dirty_regions.rects[self.dirty_regions.len - 1] = ClipRect {
1227                    x: 0,
1228                    y: 0,
1229                    w: 0,
1230                    h: 0,
1231                };
1232                self.dirty_regions.len -= 1;
1233                idx = 0;
1234                continue;
1235            }
1236            idx += 1;
1237        }
1238
1239        if self.dirty_regions.len < MAX_DIRTY_RECTS {
1240            self.dirty_regions.rects[self.dirty_regions.len] = merged;
1241            self.dirty_regions.len += 1;
1242        } else {
1243            let cur = self.dirty_regions.rects[0];
1244            let x0 = core::cmp::min(cur.x, merged.x);
1245            let y0 = core::cmp::min(cur.y, merged.y);
1246            let x1 = core::cmp::max(
1247                cur.x.saturating_add(cur.w),
1248                merged.x.saturating_add(merged.w),
1249            );
1250            let y1 = core::cmp::max(
1251                cur.y.saturating_add(cur.h),
1252                merged.y.saturating_add(merged.h),
1253            );
1254            self.dirty_regions.rects[0] = ClipRect {
1255                x: x0,
1256                y: y0,
1257                w: x1.saturating_sub(x0),
1258                h: y1.saturating_sub(y0),
1259            };
1260        }
1261    }
1262
1263    /// Sets clip rect.
1264    pub fn set_clip_rect(&mut self, x: usize, y: usize, width: usize, height: usize) {
1265        let x_end = core::cmp::min(x.saturating_add(width), self.fb_width);
1266        let y_end = core::cmp::min(y.saturating_add(height), self.fb_height);
1267        self.clip = ClipRect {
1268            x,
1269            y,
1270            w: x_end.saturating_sub(x),
1271            h: y_end.saturating_sub(y),
1272        };
1273    }
1274
1275    /// Performs the reset clip rect operation.
1276    pub fn reset_clip_rect(&mut self) {
1277        self.clip = ClipRect {
1278            x: 0,
1279            y: 0,
1280            w: self.fb_width,
1281            h: self.fb_height,
1282        };
1283    }
1284
1285    /// Performs the draw to back buffer operation.
1286    fn draw_to_back_buffer(&self) -> bool {
1287        self.draw_to_back && self.back_buffer.is_some()
1288    }
1289
1290    /// Enables double buffer.
1291    pub fn enable_double_buffer(&mut self) -> bool {
1292        if !self.enabled {
1293            return false;
1294        }
1295        if self.back_buffer.is_none() {
1296            let mut buf = Vec::with_capacity(self.fb_width.saturating_mul(self.fb_height));
1297            for y in 0..self.fb_height {
1298                for x in 0..self.fb_width {
1299                    buf.push(self.read_hw_pixel_packed(x, y));
1300                }
1301            }
1302            self.back_buffer = Some(buf);
1303        }
1304        self.draw_to_back = true;
1305        self.track_dirty = true;
1306        self.clear_dirty();
1307        true
1308    }
1309
1310    /// Disables double buffer.
1311    pub fn disable_double_buffer(&mut self, present: bool) {
1312        if present {
1313            self.present();
1314        }
1315        self.draw_to_back = false;
1316        self.track_dirty = false;
1317        self.clear_dirty();
1318    }
1319
1320    /// Performs the present operation.
1321    pub fn present(&mut self) {
1322        if !self.enabled {
1323            return;
1324        }
1325        let Some(buf) = self.back_buffer.as_ref() else {
1326            return;
1327        };
1328        if self.track_dirty && self.dirty_regions.len == 0 {
1329            return;
1330        }
1331
1332        let buf_ptr = buf.as_ptr();
1333        let bpp = self.fmt.bpp;
1334        let fb_addr = self.fb_addr;
1335        let pitch = self.pitch;
1336        let fb_width = self.fb_width;
1337        let region_count = if self.track_dirty {
1338            self.dirty_regions.len
1339        } else {
1340            1
1341        };
1342
1343        let mut regions = [ClipRect {
1344            x: 0,
1345            y: 0,
1346            w: 0,
1347            h: 0,
1348        }; MAX_DIRTY_RECTS];
1349        if self.track_dirty {
1350            let mut idx = 0;
1351            while idx < self.dirty_regions.len {
1352                regions[idx] = self.dirty_regions.rects[idx];
1353                idx += 1;
1354            }
1355        } else {
1356            regions[0] = ClipRect {
1357                x: 0,
1358                y: 0,
1359                w: self.fb_width,
1360                h: self.fb_height,
1361            };
1362        }
1363
1364        let mut region_idx = 0;
1365        while region_idx < region_count {
1366            let region = regions[region_idx];
1367            if region.w == 0 || region.h == 0 {
1368                region_idx += 1;
1369                continue;
1370            }
1371
1372            if bpp == 32 {
1373                let row_bytes = region.w * 4;
1374                for y in region.y..(region.y + region.h) {
1375                    let src = unsafe { buf_ptr.add(y * fb_width + region.x) as *const u8 };
1376                    let dst_off = y * pitch + region.x * 4;
1377                    unsafe {
1378                        core::ptr::copy_nonoverlapping(src, fb_addr.add(dst_off), row_bytes);
1379                    }
1380                }
1381            } else {
1382                for y in region.y..(region.y + region.h) {
1383                    for x in region.x..(region.x + region.w) {
1384                        let packed = unsafe { *buf_ptr.add(y * fb_width + x) };
1385                        self.write_hw_pixel_packed(x, y, packed);
1386                    }
1387                }
1388            }
1389
1390            VGA_PRESENT_REGION_COUNT.fetch_add(1, Ordering::Relaxed);
1391            VGA_PRESENT_PIXEL_COUNT
1392                .fetch_add(region.w.saturating_mul(region.h) as u64, Ordering::Relaxed);
1393            region_idx += 1;
1394        }
1395
1396        PRESENTED_FRAMES.fetch_add(1, Ordering::Relaxed);
1397        self.clear_dirty();
1398        self.present_pending = false;
1399        self.last_present_tick = crate::process::scheduler::ticks();
1400
1401        if crate::hardware::virtio::gpu::is_available() {
1402            if let Some(gpu) = crate::hardware::virtio::gpu::get_gpu() {
1403                gpu.flush_now();
1404            }
1405        }
1406
1407        if self.mc_visible {
1408            self.mc_save_hw();
1409            self.mc_draw_hw();
1410        }
1411        if self.tc_visible {
1412            self.text_cursor_save_hw();
1413            self.text_cursor_draw_hw();
1414        }
1415    }
1416
1417    fn request_present(&mut self) {
1418        if !self.draw_to_back_buffer() {
1419            return;
1420        }
1421        self.present_pending = true;
1422        self.present_if_due(false);
1423    }
1424
1425    fn present_if_due(&mut self, force: bool) {
1426        if !self.present_pending || !self.draw_to_back_buffer() {
1427            return;
1428        }
1429        let now = crate::process::scheduler::ticks();
1430        if force || now.saturating_sub(self.last_present_tick) >= PRESENT_MIN_TICKS {
1431            self.present();
1432        }
1433    }
1434
1435    // Mouse cursor ====================
1436
1437    /// Performs the mc save hw operation.
1438    fn mc_save_hw(&mut self) {
1439        let x = self.mc_x;
1440        let y = self.mc_y;
1441        let fw = self.fb_width;
1442        let fh = self.fb_height;
1443        for cy in 0..CURSOR_H {
1444            for cx in 0..CURSOR_W {
1445                let px = x + cx as i32;
1446                let py = y + cy as i32;
1447                if px < 0 || py < 0 || px as usize >= fw || py as usize >= fh {
1448                    self.mc_save[cy * CURSOR_W + cx] = 0;
1449                    continue;
1450                }
1451                self.mc_save[cy * CURSOR_W + cx] =
1452                    self.read_hw_pixel_packed(px as usize, py as usize);
1453            }
1454        }
1455    }
1456
1457    /// Performs the mc draw hw operation.
1458    fn mc_draw_hw(&mut self) {
1459        let x = self.mc_x;
1460        let y = self.mc_y;
1461        let black = self.pack_color(RgbColor::BLACK);
1462        let white = self.pack_color(RgbColor::WHITE);
1463        for cy in 0..CURSOR_H {
1464            for cx in 0..CURSOR_W {
1465                let p = CURSOR_PIXELS[cy * CURSOR_W + cx];
1466                if p == 0 {
1467                    continue;
1468                }
1469                let px = x + cx as i32;
1470                let py = y + cy as i32;
1471                if px < 0 || py < 0 || px as usize >= self.fb_width || py as usize >= self.fb_height
1472                {
1473                    continue;
1474                }
1475                let color = if p == 1 { black } else { white };
1476                self.write_hw_pixel_packed(px as usize, py as usize, color);
1477            }
1478        }
1479    }
1480
1481    /// Performs the mc erase hw operation.
1482    fn mc_erase_hw(&mut self) {
1483        let x = self.mc_x;
1484        let y = self.mc_y;
1485        for cy in 0..CURSOR_H {
1486            for cx in 0..CURSOR_W {
1487                if CURSOR_PIXELS[cy * CURSOR_W + cx] == 0 {
1488                    continue;
1489                }
1490                let px = x + cx as i32;
1491                let py = y + cy as i32;
1492                if px < 0 || py < 0 || px as usize >= self.fb_width || py as usize >= self.fb_height
1493                {
1494                    continue;
1495                }
1496                self.write_hw_pixel_packed(
1497                    px as usize,
1498                    py as usize,
1499                    self.mc_save[cy * CURSOR_W + cx],
1500                );
1501            }
1502        }
1503    }
1504
1505    /// Updates mouse cursor.
1506    pub fn update_mouse_cursor(&mut self, x: i32, y: i32) {
1507        if !self.enabled {
1508            return;
1509        }
1510        if self.mc_visible && self.mc_x == x && self.mc_y == y {
1511            self.present_if_due(false);
1512            return;
1513        }
1514        if self.mc_visible {
1515            self.mc_erase_hw();
1516        }
1517        self.mc_x = x;
1518        self.mc_y = y;
1519        self.mc_save_hw();
1520        self.mc_draw_hw();
1521        self.mc_visible = true;
1522        self.present_if_due(false);
1523    }
1524
1525    /// Performs the hide mouse cursor operation.
1526    pub fn hide_mouse_cursor(&mut self) {
1527        if self.mc_visible {
1528            self.mc_erase_hw();
1529            self.mc_visible = false;
1530        }
1531        self.present_if_due(false);
1532    }
1533
1534    fn text_cursor_rect(&self) -> Option<(usize, usize, usize, usize)> {
1535        if !self.enabled {
1536            return None;
1537        }
1538        let (gw, gh) = self.glyph_size();
1539        if gw == 0 || gh == 0 {
1540            return None;
1541        }
1542        let x = self.tc_col.saturating_mul(gw);
1543        let y = self.tc_row.saturating_mul(gh);
1544        if x >= self.fb_width || y >= self.fb_height {
1545            return None;
1546        }
1547        Some((
1548            x,
1549            y,
1550            gw.min(TEXT_CURSOR_MAX_DIM).min(self.fb_width - x),
1551            gh.min(TEXT_CURSOR_MAX_DIM).min(self.fb_height - y),
1552        ))
1553    }
1554
1555    fn text_cursor_save_hw(&mut self) {
1556        let Some((x, y, w, h)) = self.text_cursor_rect() else {
1557            self.tc_w = 0;
1558            self.tc_h = 0;
1559            return;
1560        };
1561        self.tc_w = w;
1562        self.tc_h = h;
1563        for cy in 0..h {
1564            for cx in 0..w {
1565                self.tc_save[cy * TEXT_CURSOR_MAX_DIM + cx] =
1566                    self.read_hw_pixel_packed(x + cx, y + cy);
1567            }
1568        }
1569    }
1570
1571    fn text_cursor_draw_hw(&mut self) {
1572        let Some((x, y, w, h)) = self.text_cursor_rect() else {
1573            return;
1574        };
1575        for cy in 0..h {
1576            for cx in 0..w {
1577                self.write_hw_pixel_packed(x + cx, y + cy, self.tc_color);
1578            }
1579        }
1580    }
1581
1582    fn text_cursor_erase_hw(&mut self) {
1583        let Some((x, y, w, h)) = self.text_cursor_rect() else {
1584            return;
1585        };
1586        let restore_w = w.min(self.tc_w);
1587        let restore_h = h.min(self.tc_h);
1588        for cy in 0..restore_h {
1589            for cx in 0..restore_w {
1590                self.write_hw_pixel_packed(
1591                    x + cx,
1592                    y + cy,
1593                    self.tc_save[cy * TEXT_CURSOR_MAX_DIM + cx],
1594                );
1595            }
1596        }
1597    }
1598
1599    fn draw_text_cursor_overlay(&mut self, color: RgbColor) {
1600        if !self.enabled {
1601            return;
1602        }
1603        let packed = self.pack_color(color);
1604        if self.tc_visible {
1605            self.text_cursor_erase_hw();
1606            self.tc_visible = false;
1607        }
1608        if packed == self.bg {
1609            self.present_if_due(false);
1610            return;
1611        }
1612        self.tc_col = self.col;
1613        self.tc_row = self.row;
1614        self.tc_color = packed;
1615        self.text_cursor_save_hw();
1616        self.text_cursor_draw_hw();
1617        self.tc_visible = true;
1618        self.present_if_due(false);
1619    }
1620
1621    //  Text selection ==========
1622
1623    /// Performs the sel normalized operation.
1624    fn sel_normalized(&self) -> (usize, usize, usize, usize) {
1625        let (sr, sc, er, ec) = (
1626            self.sel_start_row,
1627            self.sel_start_col,
1628            self.sel_end_row,
1629            self.sel_end_col,
1630        );
1631        if sr < er || (sr == er && sc <= ec) {
1632            (sr, sc, er, ec)
1633        } else {
1634            (er, ec, sr, sc)
1635        }
1636    }
1637
1638    /// Performs the pixel to sb pos operation.
1639    pub fn pixel_to_sb_pos(&self, px: usize, py: usize) -> Option<(usize, usize)> {
1640        if !self.enabled {
1641            return None;
1642        }
1643        let gw = self.font_info.glyph_w;
1644        let gh = self.font_info.glyph_h;
1645        if gw == 0 || gh == 0 {
1646            return None;
1647        }
1648        let text_h = self.text_area_height();
1649        let text_w = self.fb_width.saturating_sub(SCROLLBAR_W);
1650        if px >= text_w || py >= text_h {
1651            return None;
1652        }
1653        let vis_row = py / gh;
1654        let vis_col = px / gw;
1655        if vis_col >= self.cols {
1656            return None;
1657        }
1658        let total_complete = self.sb_rows.len();
1659        let has_partial = !self.sb_cur_row.is_empty();
1660        let total_virtual = total_complete + if has_partial { 1 } else { 0 };
1661        if total_virtual == 0 {
1662            return None;
1663        }
1664        let view_end = total_virtual.saturating_sub(self.scroll_offset);
1665        let view_start = view_end.saturating_sub(self.rows);
1666        let display_len = view_end.saturating_sub(view_start);
1667        if vis_row >= display_len {
1668            return None;
1669        }
1670        Some((view_start + vis_row, vis_col))
1671    }
1672
1673    /// Starts selection.
1674    pub fn start_selection(&mut self, px: usize, py: usize) {
1675        if let Some((row, col)) = self.pixel_to_sb_pos(px, py) {
1676            self.sel_start_row = row;
1677            self.sel_start_col = col;
1678            self.sel_end_row = row;
1679            self.sel_end_col = col;
1680            self.sel_active = true;
1681            self.render_viewport_full();
1682        }
1683    }
1684
1685    /// Updates selection.
1686    pub fn update_selection(&mut self, px: usize, py: usize) {
1687        if !self.sel_active {
1688            return;
1689        }
1690        if let Some((row, col)) = self.pixel_to_sb_pos(px, py) {
1691            if row == self.sel_end_row && col == self.sel_end_col {
1692                return;
1693            }
1694            self.sel_end_row = row;
1695            self.sel_end_col = col;
1696            self.render_viewport_full();
1697        }
1698    }
1699
1700    /// Performs the end selection operation.
1701    pub fn end_selection(&mut self) {
1702        if !self.sel_active {
1703            return;
1704        }
1705        let (start_row, start_col, end_row, end_col) = self.sel_normalized();
1706        let mut bytes: alloc::vec::Vec<u8> = alloc::vec::Vec::new();
1707        for row in start_row..=end_row {
1708            let len = if row < self.sb_rows.len() {
1709                self.sb_row_at(row).map(|r| r.len()).unwrap_or(0)
1710            } else if row == self.sb_rows.len() {
1711                self.sb_cur_row.len()
1712            } else {
1713                break;
1714            };
1715            let c0 = if row == start_row {
1716                start_col.min(len)
1717            } else {
1718                0
1719            };
1720            let c1 = if row == end_row {
1721                end_col.min(len)
1722            } else {
1723                len
1724            };
1725            for col in c0..c1 {
1726                let ch = if row < self.sb_rows.len() {
1727                    self.sb_row_at(row)
1728                        .and_then(|r| r.get(col))
1729                        .map(|cell| cell.ch)
1730                        .unwrap_or(' ')
1731                } else {
1732                    self.sb_cur_row[col].ch
1733                };
1734                let mut buf = [0u8; 4];
1735                bytes.extend_from_slice(ch.encode_utf8(&mut buf).as_bytes());
1736            }
1737            if row < end_row {
1738                bytes.push(b'\n');
1739            }
1740        }
1741        if let Some(mut clip) = CLIPBOARD.try_lock() {
1742            let n = bytes.len().min(CLIPBOARD_CAP);
1743            clip.0[..n].copy_from_slice(&bytes[..n]);
1744            clip.1 = n;
1745        }
1746    }
1747
1748    /// Performs the clear selection operation.
1749    pub fn clear_selection(&mut self) {
1750        if self.sel_active {
1751            self.sel_active = false;
1752            self.render_viewport_full();
1753        }
1754    }
1755
1756    /// Performs the clear with operation.
1757    pub fn clear_with(&mut self, color: RgbColor) {
1758        if !self.enabled {
1759            return;
1760        }
1761        let packed = self.pack_color(color);
1762        for y in 0..self.fb_height {
1763            for x in 0..self.fb_width {
1764                self.put_pixel_raw(x, y, packed);
1765            }
1766        }
1767        self.col = 0;
1768        self.row = 0;
1769    }
1770
1771    /// Performs the clear operation.
1772    pub fn clear(&mut self) {
1773        self.clear_with(self.unpack_color(self.bg));
1774    }
1775
1776    /// Performs the pixel offset operation.
1777    #[inline]
1778    fn pixel_offset(&self, x: usize, y: usize) -> Option<usize> {
1779        if x >= self.fb_width || y >= self.fb_height {
1780            return None;
1781        }
1782        let bytes_pp = self.fmt.bpp as usize / 8;
1783        let row = y.checked_mul(self.pitch)?;
1784        let col = x.checked_mul(bytes_pp)?;
1785        row.checked_add(col)
1786    }
1787
1788    /// Writes hw pixel packed.
1789    fn write_hw_pixel_packed(&mut self, x: usize, y: usize, color: u32) {
1790        let Some(off) = self.pixel_offset(x, y) else {
1791            return;
1792        };
1793        unsafe {
1794            match self.fmt.bpp {
1795                32 => {
1796                    core::ptr::write_volatile(self.fb_addr.add(off) as *mut u32, color);
1797                }
1798                24 => {
1799                    core::ptr::write_volatile(self.fb_addr.add(off), (color & 0xFF) as u8);
1800                    core::ptr::write_volatile(
1801                        self.fb_addr.add(off + 1),
1802                        ((color >> 8) & 0xFF) as u8,
1803                    );
1804                    core::ptr::write_volatile(
1805                        self.fb_addr.add(off + 2),
1806                        ((color >> 16) & 0xFF) as u8,
1807                    );
1808                }
1809                _ => {}
1810            }
1811        }
1812    }
1813
1814    /// Reads hw pixel packed.
1815    fn read_hw_pixel_packed(&self, x: usize, y: usize) -> u32 {
1816        let Some(off) = self.pixel_offset(x, y) else {
1817            return 0;
1818        };
1819        unsafe {
1820            match self.fmt.bpp {
1821                32 => core::ptr::read_volatile(self.fb_addr.add(off) as *const u32),
1822                24 => {
1823                    let b0 = core::ptr::read_volatile(self.fb_addr.add(off)) as u32;
1824                    let b1 = core::ptr::read_volatile(self.fb_addr.add(off + 1)) as u32;
1825                    let b2 = core::ptr::read_volatile(self.fb_addr.add(off + 2)) as u32;
1826                    b0 | (b1 << 8) | (b2 << 16)
1827                }
1828                _ => 0,
1829            }
1830        }
1831    }
1832
1833    /// Reads pixel packed.
1834    fn read_pixel_packed(&self, x: usize, y: usize) -> u32 {
1835        if self.draw_to_back_buffer() {
1836            if let Some(buf) = self.back_buffer.as_ref() {
1837                return buf[y * self.fb_width + x];
1838            }
1839        }
1840        self.read_hw_pixel_packed(x, y)
1841    }
1842
1843    /// Performs the put pixel raw operation.
1844    fn put_pixel_raw(&mut self, x: usize, y: usize, color: u32) {
1845        if !self.enabled || x >= self.fb_width || y >= self.fb_height || !self.in_clip(x, y) {
1846            return;
1847        }
1848        if self.draw_to_back_buffer() {
1849            if let Some(buf) = self.back_buffer.as_mut() {
1850                buf[y * self.fb_width + x] = color;
1851                self.mark_dirty_rect(x, y, 1, 1);
1852                return;
1853            }
1854        }
1855        self.write_hw_pixel_packed(x, y, color);
1856    }
1857
1858    /// Performs the draw pixel operation.
1859    pub fn draw_pixel(&mut self, x: usize, y: usize, color: RgbColor) {
1860        self.put_pixel_raw(x, y, self.pack_color(color));
1861    }
1862
1863    /// Performs the draw pixel alpha operation.
1864    pub fn draw_pixel_alpha(&mut self, x: usize, y: usize, color: RgbColor, alpha: u8) {
1865        if !self.enabled
1866            || alpha == 0
1867            || x >= self.fb_width
1868            || y >= self.fb_height
1869            || !self.in_clip(x, y)
1870        {
1871            return;
1872        }
1873        if alpha == 255 {
1874            self.put_pixel_raw(x, y, self.pack_color(color));
1875            return;
1876        }
1877        let dst = self.unpack_color(self.read_pixel_packed(x, y));
1878        let inv = (255u16).saturating_sub(alpha as u16);
1879        let a = alpha as u16;
1880        let blended = RgbColor::new(
1881            ((color.r as u16 * a + dst.r as u16 * inv + 127) / 255) as u8,
1882            ((color.g as u16 * a + dst.g as u16 * inv + 127) / 255) as u8,
1883            ((color.b as u16 * a + dst.b as u16 * inv + 127) / 255) as u8,
1884        );
1885        self.put_pixel_raw(x, y, self.pack_color(blended));
1886    }
1887
1888    /// Performs the draw line operation.
1889    pub fn draw_line(&mut self, x0: isize, y0: isize, x1: isize, y1: isize, color: RgbColor) {
1890        let mut x = x0;
1891        let mut y = y0;
1892        let dx = (x1 - x0).abs();
1893        let sx = if x0 < x1 { 1 } else { -1 };
1894        let dy = -(y1 - y0).abs();
1895        let sy = if y0 < y1 { 1 } else { -1 };
1896        let mut err = dx + dy;
1897        let packed = self.pack_color(color);
1898
1899        loop {
1900            if x >= 0 && y >= 0 {
1901                self.put_pixel_raw(x as usize, y as usize, packed);
1902            }
1903            if x == x1 && y == y1 {
1904                break;
1905            }
1906            let e2 = 2 * err;
1907            if e2 >= dy {
1908                err += dy;
1909                x += sx;
1910            }
1911            if e2 <= dx {
1912                err += dx;
1913                y += sy;
1914            }
1915        }
1916    }
1917
1918    /// Performs the draw rect operation.
1919    pub fn draw_rect(&mut self, x: usize, y: usize, width: usize, height: usize, color: RgbColor) {
1920        if width == 0 || height == 0 {
1921            return;
1922        }
1923        let x2 = x.saturating_add(width - 1);
1924        let y2 = y.saturating_add(height - 1);
1925        self.draw_line(x as isize, y as isize, x2 as isize, y as isize, color);
1926        self.draw_line(x as isize, y as isize, x as isize, y2 as isize, color);
1927        self.draw_line(x2 as isize, y as isize, x2 as isize, y2 as isize, color);
1928        self.draw_line(x as isize, y2 as isize, x2 as isize, y2 as isize, color);
1929    }
1930
1931    /// Performs the fill rect operation.
1932    pub fn fill_rect(&mut self, x: usize, y: usize, width: usize, height: usize, color: RgbColor) {
1933        let Some((sx, sy, sw, sh)) = self.clipped_rect(x, y, width, height) else {
1934            return;
1935        };
1936        let packed = self.pack_color(color);
1937
1938        if self.draw_to_back_buffer() {
1939            if let Some(buf) = self.back_buffer.as_mut() {
1940                for py in sy..(sy + sh) {
1941                    let row = py * self.fb_width;
1942                    let start = row + sx;
1943                    let end = start + sw;
1944                    buf[start..end].fill(packed);
1945                }
1946                self.mark_dirty_rect(sx, sy, sw, sh);
1947                return;
1948            }
1949        }
1950
1951        if self.fmt.bpp == 32 {
1952            for py in sy..(sy + sh) {
1953                let Some(row_off) = py
1954                    .checked_mul(self.pitch)
1955                    .and_then(|v| v.checked_add(sx * 4))
1956                else {
1957                    continue;
1958                };
1959                let count = sw;
1960                unsafe {
1961                    let ptr = self.fb_addr.add(row_off) as *mut u32;
1962                    for i in 0..count {
1963                        core::ptr::write_volatile(ptr.add(i), packed);
1964                    }
1965                }
1966            }
1967            return;
1968        }
1969
1970        for py in sy..(sy + sh) {
1971            for px in sx..(sx + sw) {
1972                self.write_hw_pixel_packed(px, py, packed);
1973            }
1974        }
1975    }
1976
1977    /// Performs the fill rect alpha operation.
1978    pub fn fill_rect_alpha(
1979        &mut self,
1980        x: usize,
1981        y: usize,
1982        width: usize,
1983        height: usize,
1984        color: RgbColor,
1985        alpha: u8,
1986    ) {
1987        if !self.enabled || width == 0 || height == 0 || alpha == 0 {
1988            return;
1989        }
1990        if alpha == 255 {
1991            self.fill_rect(x, y, width, height, color);
1992            return;
1993        }
1994        let x_end = core::cmp::min(x.saturating_add(width), self.fb_width);
1995        let y_end = core::cmp::min(y.saturating_add(height), self.fb_height);
1996        for py in y..y_end {
1997            for px in x..x_end {
1998                self.draw_pixel_alpha(px, py, color, alpha);
1999            }
2000        }
2001    }
2002
2003    /// Performs the blit rgb operation.
2004    pub fn blit_rgb(
2005        &mut self,
2006        dst_x: usize,
2007        dst_y: usize,
2008        src_width: usize,
2009        src_height: usize,
2010        pixels: &[RgbColor],
2011    ) -> bool {
2012        let len = src_width.saturating_mul(src_height);
2013        if !self.enabled || src_width == 0 || src_height == 0 || pixels.len() < len {
2014            return false;
2015        }
2016        let x_end = core::cmp::min(dst_x.saturating_add(src_width), self.fb_width);
2017        let y_end = core::cmp::min(dst_y.saturating_add(src_height), self.fb_height);
2018        if x_end <= dst_x || y_end <= dst_y {
2019            return true;
2020        }
2021        let copy_w = x_end - dst_x;
2022        let copy_h = y_end - dst_y;
2023        for row in 0..copy_h {
2024            let src_row = row * src_width;
2025            for col in 0..copy_w {
2026                self.draw_pixel(dst_x + col, dst_y + row, pixels[src_row + col]);
2027            }
2028        }
2029        true
2030    }
2031
2032    /// Performs the blit rgb24 operation.
2033    pub fn blit_rgb24(
2034        &mut self,
2035        dst_x: usize,
2036        dst_y: usize,
2037        src_width: usize,
2038        src_height: usize,
2039        bytes: &[u8],
2040    ) -> bool {
2041        let needed = src_width.saturating_mul(src_height).saturating_mul(3);
2042        if !self.enabled || src_width == 0 || src_height == 0 || bytes.len() < needed {
2043            return false;
2044        }
2045        let x_end = core::cmp::min(dst_x.saturating_add(src_width), self.fb_width);
2046        let y_end = core::cmp::min(dst_y.saturating_add(src_height), self.fb_height);
2047        if x_end <= dst_x || y_end <= dst_y {
2048            return true;
2049        }
2050        let copy_w = x_end - dst_x;
2051        let copy_h = y_end - dst_y;
2052        for row in 0..copy_h {
2053            for col in 0..copy_w {
2054                let i = (row * src_width + col) * 3;
2055                let color = RgbColor::new(bytes[i], bytes[i + 1], bytes[i + 2]);
2056                self.draw_pixel(dst_x + col, dst_y + row, color);
2057            }
2058        }
2059        true
2060    }
2061
2062    /// Performs the blit rgba operation.
2063    pub fn blit_rgba(
2064        &mut self,
2065        dst_x: usize,
2066        dst_y: usize,
2067        src_width: usize,
2068        src_height: usize,
2069        bytes: &[u8],
2070        global_alpha: u8,
2071    ) -> bool {
2072        let needed = src_width.saturating_mul(src_height).saturating_mul(4);
2073        if !self.enabled
2074            || src_width == 0
2075            || src_height == 0
2076            || bytes.len() < needed
2077            || global_alpha == 0
2078        {
2079            return false;
2080        }
2081
2082        let Some((sx, sy, sw, sh)) = self.clipped_rect(dst_x, dst_y, src_width, src_height) else {
2083            return true;
2084        };
2085        let src_x0 = sx.saturating_sub(dst_x);
2086        let src_y0 = sy.saturating_sub(dst_y);
2087
2088        for row in 0..sh {
2089            let syi = src_y0 + row;
2090            for col in 0..sw {
2091                let sxi = src_x0 + col;
2092                let i = (syi * src_width + sxi) * 4;
2093                let r = bytes[i];
2094                let g = bytes[i + 1];
2095                let b = bytes[i + 2];
2096                let sa = bytes[i + 3];
2097                if sa == 0 {
2098                    continue;
2099                }
2100                let a = ((sa as u16 * global_alpha as u16 + 127) / 255) as u8;
2101                let dx = sx + col;
2102                let dy = sy + row;
2103                if a == 255 {
2104                    self.put_pixel_raw(dx, dy, self.pack_color(RgbColor::new(r, g, b)));
2105                } else if a != 0 {
2106                    self.draw_pixel_alpha(dx, dy, RgbColor::new(r, g, b), a);
2107                }
2108            }
2109        }
2110        true
2111    }
2112
2113    /// Performs the blit sprite rgba operation.
2114    pub fn blit_sprite_rgba(
2115        &mut self,
2116        dst_x: usize,
2117        dst_y: usize,
2118        sprite: SpriteRgba<'_>,
2119        global_alpha: u8,
2120    ) -> bool {
2121        self.blit_rgba(
2122            dst_x,
2123            dst_y,
2124            sprite.width,
2125            sprite.height,
2126            sprite.pixels,
2127            global_alpha,
2128        )
2129    }
2130
2131    /// Performs the draw text at operation.
2132    pub fn draw_text_at(
2133        &mut self,
2134        pixel_x: usize,
2135        pixel_y: usize,
2136        text: &str,
2137        fg: RgbColor,
2138        bg: RgbColor,
2139    ) {
2140        if !self.enabled {
2141            return;
2142        }
2143        let gw = self.font_info.glyph_w;
2144        let gh = self.font_info.glyph_h;
2145        let fg_packed = self.pack_color(fg);
2146        let bg_packed = self.pack_color(bg);
2147        let mut cx = pixel_x;
2148        let cy = pixel_y;
2149        for ch in text.chars() {
2150            if ch == '\n' {
2151                break;
2152            }
2153            if cx + gw > self.fb_width || cy + gh > self.fb_height {
2154                break;
2155            }
2156            self.draw_glyph_at_pixel(cx, cy, ch, fg_packed, bg_packed);
2157            cx += gw;
2158        }
2159    }
2160
2161    /// Performs the glyph index for char operation.
2162    fn glyph_index_for_char(&self, ch: char) -> usize {
2163        if ch.is_ascii() {
2164            let idx = ch as usize;
2165            if idx < self.font_info.glyph_count {
2166                return idx;
2167            }
2168        }
2169        let cp = ch as u32;
2170        if let Some((_, glyph)) = self.unicode_map.iter().find(|(u, _)| *u == cp) {
2171            return *glyph;
2172        }
2173        if let Some((_, glyph)) = self.unicode_map.iter().find(|(u, _)| *u == ('?' as u32)) {
2174            return *glyph;
2175        }
2176        if ('?' as usize) < self.font_info.glyph_count {
2177            return '?' as usize;
2178        }
2179        0
2180    }
2181
2182    /// Performs the draw glyph index at pixel operation.
2183    fn draw_glyph_index_at_pixel(
2184        &mut self,
2185        pixel_x: usize,
2186        pixel_y: usize,
2187        glyph_index: usize,
2188        fg: u32,
2189        bg: u32,
2190    ) {
2191        if !self.enabled {
2192            return;
2193        }
2194        let glyph_index = core::cmp::min(glyph_index, self.font_info.glyph_count.saturating_sub(1));
2195        let gw = self.font_info.glyph_w;
2196        let gh = self.font_info.glyph_h;
2197        let glyph_pixels = gw.saturating_mul(gh);
2198        let Some(mask) = self.glyph_mask_slice(glyph_index) else {
2199            return;
2200        };
2201        let mask_ptr = mask.as_ptr();
2202
2203        if self.draw_to_back_buffer()
2204            && pixel_x + gw <= self.fb_width
2205            && pixel_y + gh <= self.fb_height
2206        {
2207            let fb_width = self.fb_width;
2208            if let Some(buf) = self.back_buffer.as_mut() {
2209                for gy in 0..gh {
2210                    let row_start = (pixel_y + gy) * fb_width + pixel_x;
2211                    buf[row_start..row_start + gw].fill(bg);
2212                    for gx in 0..gw {
2213                        let idx = gy * gw + gx;
2214                        if idx >= glyph_pixels {
2215                            continue;
2216                        }
2217                        let bit = unsafe { *mask_ptr.add(idx) };
2218                        if bit != 0 {
2219                            buf[row_start + gx] = fg;
2220                        }
2221                    }
2222                }
2223            }
2224            self.mark_dirty_rect(pixel_x, pixel_y, gw, gh);
2225        } else {
2226            for gy in 0..gh {
2227                for gx in 0..gw {
2228                    let idx = gy * gw + gx;
2229                    if idx >= glyph_pixels {
2230                        continue;
2231                    }
2232                    let color = if unsafe { *mask_ptr.add(idx) } != 0 {
2233                        fg
2234                    } else {
2235                        bg
2236                    };
2237                    self.put_pixel_raw(pixel_x + gx, pixel_y + gy, color);
2238                }
2239            }
2240        }
2241    }
2242
2243    /// Performs the draw glyph at pixel operation.
2244    fn draw_glyph_at_pixel(&mut self, pixel_x: usize, pixel_y: usize, ch: char, fg: u32, bg: u32) {
2245        let glyph_index = self.glyph_index_for_char(ch);
2246        self.draw_glyph_index_at_pixel(pixel_x, pixel_y, glyph_index, fg, bg);
2247    }
2248
2249    fn fill_text_span_bg(
2250        &mut self,
2251        pixel_x: usize,
2252        pixel_y: usize,
2253        width: usize,
2254        height: usize,
2255        bg: u32,
2256    ) {
2257        if self.draw_to_back_buffer()
2258            && pixel_x + width <= self.fb_width
2259            && pixel_y + height <= self.fb_height
2260        {
2261            let fb_width = self.fb_width;
2262            if let Some(buf) = self.back_buffer.as_mut() {
2263                for row in 0..height {
2264                    let start = (pixel_y + row) * fb_width + pixel_x;
2265                    let end = start + width;
2266                    buf[start..end].fill(bg);
2267                }
2268                self.mark_dirty_rect(pixel_x, pixel_y, width, height);
2269                return;
2270            }
2271        }
2272
2273        let gh = height;
2274        let color = self.unpack_color(bg);
2275        self.fill_rect(pixel_x, pixel_y, width, gh, color);
2276    }
2277
2278    fn clear_text_line_pixels(&mut self, vis_row: usize) {
2279        let gh = self.font_info.glyph_h;
2280        let text_w = self.fb_width.saturating_sub(SCROLLBAR_W);
2281        self.fill_text_span_bg(0, vis_row.saturating_mul(gh), text_w, gh, self.bg);
2282    }
2283
2284    fn visible_virtual_bounds(&self) -> (usize, usize, usize, usize, bool) {
2285        let total_complete = self.sb_rows.len();
2286        let has_partial = !self.sb_cur_row.is_empty();
2287        let total_virtual = total_complete + if has_partial { 1 } else { 0 };
2288        let view_end = total_virtual.saturating_sub(self.scroll_offset);
2289        let view_start = view_end.saturating_sub(self.rows);
2290        (
2291            total_complete,
2292            total_virtual,
2293            view_start,
2294            view_end,
2295            has_partial,
2296        )
2297    }
2298
2299    fn sync_live_cursor_from_view(
2300        &mut self,
2301        total_complete: usize,
2302        view_start: usize,
2303        view_end: usize,
2304    ) {
2305        let display_len = view_end.saturating_sub(view_start);
2306        self.row = if display_len > 0 { display_len - 1 } else { 0 };
2307        let last_virt = view_start + display_len.saturating_sub(1);
2308        let last_len = if last_virt < total_complete {
2309            self.sb_row_at(last_virt).map(|row| row.len()).unwrap_or(0)
2310        } else if last_virt == total_complete {
2311            self.sb_cur_row.len()
2312        } else {
2313            0
2314        };
2315        self.col = last_len.min(self.cols);
2316    }
2317
2318    fn selection_colors_for_cell(
2319        &self,
2320        virt_row: usize,
2321        col: usize,
2322        fg: u32,
2323        bg: u32,
2324    ) -> (u32, u32) {
2325        if !self.sel_active {
2326            return (fg, bg);
2327        }
2328        let (sel_sr, sel_sc, sel_er, sel_ec) = self.sel_normalized();
2329        let in_sel = if virt_row < sel_sr || virt_row > sel_er {
2330            false
2331        } else if sel_sr == sel_er {
2332            col >= sel_sc && col < sel_ec
2333        } else if virt_row == sel_sr {
2334            col >= sel_sc
2335        } else if virt_row == sel_er {
2336            col < sel_ec
2337        } else {
2338            true
2339        };
2340        if in_sel {
2341            (
2342                self.pack_color(RgbColor::WHITE),
2343                self.pack_color(RgbColor::new(0x26, 0x5F, 0xCC)),
2344            )
2345        } else {
2346            (fg, bg)
2347        }
2348    }
2349
2350    fn render_virtual_row(
2351        &mut self,
2352        virt_row: usize,
2353        vis_row: usize,
2354        total_complete: usize,
2355        has_partial: bool,
2356    ) {
2357        let glyph_h = self.font_info.glyph_h;
2358        let glyph_w = self.font_info.glyph_w;
2359        let py = vis_row.saturating_mul(glyph_h);
2360
2361        let (row_ptr, row_len) = if virt_row < total_complete {
2362            let row = match self.sb_row_at(virt_row) {
2363                Some(row) => row,
2364                None => return,
2365            };
2366            (row.as_ptr(), row.len())
2367        } else if has_partial && virt_row == total_complete {
2368            (self.sb_cur_row.as_ptr(), self.sb_cur_row.len())
2369        } else {
2370            (core::ptr::null(), 0)
2371        };
2372
2373        let cell_count = row_len.min(self.cols);
2374        let text_w = self.fb_width.saturating_sub(SCROLLBAR_W);
2375        let used_width = cell_count.saturating_mul(glyph_w).min(text_w);
2376
2377        if self.draw_to_back_buffer() && py + glyph_h <= self.fb_height {
2378            let fb_width = self.fb_width;
2379            let default_bg = self.bg;
2380            let glyph_pixels = glyph_w.saturating_mul(glyph_h);
2381            let glyph_cache_ptr = self.glyph_mask_cache.as_ptr();
2382            self.prepared_row_cells.clear();
2383            if self.prepared_row_cells.capacity() < cell_count {
2384                self.prepared_row_cells
2385                    .reserve(cell_count - self.prepared_row_cells.capacity());
2386            }
2387            for col in 0..cell_count {
2388                let cell = unsafe { &*row_ptr.add(col) };
2389                let glyph_index = self.glyph_index_for_char(cell.ch);
2390                let (draw_fg, draw_bg) =
2391                    self.selection_colors_for_cell(virt_row, col, cell.fg, cell.bg);
2392                self.prepared_row_cells
2393                    .push((glyph_index, draw_fg, draw_bg));
2394            }
2395            if let Some(buf) = self.back_buffer.as_mut() {
2396                for gy in 0..glyph_h {
2397                    let row_start = (py + gy) * fb_width;
2398                    buf[row_start..row_start + text_w].fill(default_bg);
2399                }
2400
2401                for (col, (glyph_index, draw_fg, draw_bg)) in
2402                    self.prepared_row_cells.iter().copied().enumerate()
2403                {
2404                    let px = col.saturating_mul(glyph_w);
2405                    if draw_bg != default_bg {
2406                        for gy in 0..glyph_h {
2407                            let row_start = (py + gy) * fb_width + px;
2408                            buf[row_start..row_start + glyph_w].fill(draw_bg);
2409                        }
2410                    }
2411
2412                    let glyph_base = glyph_index.saturating_mul(glyph_pixels);
2413                    for gy in 0..glyph_h {
2414                        let row_start = (py + gy) * fb_width + px;
2415                        for gx in 0..glyph_w {
2416                            let idx = glyph_base + gy * glyph_w + gx;
2417                            let bit = unsafe { *glyph_cache_ptr.add(idx) };
2418                            if bit != 0 {
2419                                buf[row_start + gx] = draw_fg;
2420                            }
2421                        }
2422                    }
2423                }
2424            }
2425
2426            self.mark_dirty_rect(0, py, text_w, glyph_h);
2427            return;
2428        }
2429
2430        for col in 0..cell_count {
2431            let px = col.saturating_mul(glyph_w);
2432            let cell = unsafe { &*row_ptr.add(col) };
2433            let (draw_fg, draw_bg) =
2434                self.selection_colors_for_cell(virt_row, col, cell.fg, cell.bg);
2435            self.draw_glyph_at_pixel(px, py, cell.ch, draw_fg, draw_bg);
2436        }
2437        if text_w > used_width {
2438            self.fill_text_span_bg(used_width, py, text_w - used_width, glyph_h, self.bg);
2439        }
2440    }
2441
2442    fn redraw_visible_rows(&mut self, start_vis_row: usize, count: usize) {
2443        let (total_complete, _, view_start, view_end, has_partial) = self.visible_virtual_bounds();
2444        let display_len = view_end.saturating_sub(view_start);
2445        let end_vis_row = start_vis_row.saturating_add(count).min(self.rows);
2446        for vis_row in start_vis_row..end_vis_row {
2447            if vis_row < display_len {
2448                self.render_virtual_row(view_start + vis_row, vis_row, total_complete, has_partial);
2449            } else {
2450                self.clear_text_line_pixels(vis_row);
2451            }
2452        }
2453    }
2454
2455    fn ensure_back_buffer_for_viewport(&mut self) {
2456        if self.back_buffer.is_none() {
2457            let total = self.fb_width.saturating_mul(self.fb_height);
2458            if total > 0 {
2459                self.back_buffer = Some(alloc::vec![0u32; total]);
2460            }
2461        }
2462    }
2463
2464    fn begin_viewport_render(&mut self) -> (bool, bool) {
2465        self.ensure_back_buffer_for_viewport();
2466        let prev_draw_to_back = self.draw_to_back;
2467        let prev_track_dirty = self.track_dirty;
2468        if self.back_buffer.is_some() {
2469            self.draw_to_back = true;
2470            self.track_dirty = true;
2471            self.clear_dirty();
2472        }
2473        (prev_draw_to_back, prev_track_dirty)
2474    }
2475
2476    fn end_viewport_render(&mut self, prev_draw_to_back: bool, prev_track_dirty: bool) {
2477        if self.back_buffer.is_some() {
2478            self.request_present();
2479            let now = crate::process::scheduler::ticks();
2480            let force_present = self.last_present_tick == 0 || now == 0;
2481            self.present_if_due(force_present);
2482            self.draw_to_back = prev_draw_to_back;
2483            self.track_dirty = prev_track_dirty;
2484            if !prev_track_dirty {
2485                self.clear_dirty();
2486            }
2487        }
2488    }
2489
2490    fn finalize_live_view_state(&mut self) {
2491        let (total_complete, _, view_start, view_end, _) = self.visible_virtual_bounds();
2492        if self.scroll_offset == 0 {
2493            self.sync_live_cursor_from_view(total_complete, view_start, view_end);
2494        }
2495    }
2496
2497    fn render_viewport_full(&mut self) {
2498        let (prev_draw_to_back, prev_track_dirty) = self.begin_viewport_render();
2499        let text_h = self.text_area_height();
2500        let text_w = self.fb_width.saturating_sub(SCROLLBAR_W);
2501        self.fill_rect(0, 0, text_w, text_h, self.unpack_color(self.bg));
2502        self.redraw_visible_rows(0, self.rows);
2503        self.finalize_live_view_state();
2504        self.draw_scrollbar_inner();
2505        self.end_viewport_render(prev_draw_to_back, prev_track_dirty);
2506    }
2507
2508    fn refresh_viewport_decorations(&mut self) {
2509        let (prev_draw_to_back, prev_track_dirty) = self.begin_viewport_render();
2510        self.finalize_live_view_state();
2511        self.draw_scrollbar_inner();
2512        self.end_viewport_render(prev_draw_to_back, prev_track_dirty);
2513    }
2514
2515    fn set_scroll_offset_and_render(&mut self, new_offset: usize) {
2516        let old_offset = self.scroll_offset;
2517        self.scroll_offset = new_offset;
2518        if !self.redraw_from_scrollback_incremental(old_offset) {
2519            self.redraw_from_scrollback();
2520        }
2521    }
2522
2523    fn move_text_view_pixels_up(&mut self, pixels: usize) {
2524        if pixels == 0 {
2525            return;
2526        }
2527        let text_h = self.text_area_height();
2528        let text_w = self.fb_width.saturating_sub(SCROLLBAR_W);
2529        if pixels >= text_h {
2530            self.fill_rect(0, 0, text_w, text_h, self.unpack_color(self.bg));
2531            return;
2532        }
2533        let move_rows = text_h - pixels;
2534        if self.draw_to_back_buffer() {
2535            if let Some(buf) = self.back_buffer.as_mut() {
2536                let row_width = self.fb_width;
2537                buf.copy_within(pixels * row_width..text_h * row_width, 0);
2538                self.mark_dirty_rect(0, 0, self.fb_width, text_h);
2539            }
2540        } else {
2541            unsafe {
2542                core::ptr::copy(
2543                    self.fb_addr.add(pixels * self.pitch),
2544                    self.fb_addr,
2545                    move_rows * self.pitch,
2546                );
2547            }
2548        }
2549        self.fill_rect(0, move_rows, text_w, pixels, self.unpack_color(self.bg));
2550    }
2551
2552    fn move_text_view_pixels_down(&mut self, pixels: usize) {
2553        if pixels == 0 {
2554            return;
2555        }
2556        let text_h = self.text_area_height();
2557        let text_w = self.fb_width.saturating_sub(SCROLLBAR_W);
2558        if pixels >= text_h {
2559            self.fill_rect(0, 0, text_w, text_h, self.unpack_color(self.bg));
2560            return;
2561        }
2562        let move_rows = text_h - pixels;
2563        if self.draw_to_back_buffer() {
2564            if let Some(buf) = self.back_buffer.as_mut() {
2565                let row_width = self.fb_width;
2566                buf.copy_within(0..move_rows * row_width, pixels * row_width);
2567                self.mark_dirty_rect(0, 0, self.fb_width, text_h);
2568            }
2569        } else {
2570            unsafe {
2571                core::ptr::copy(
2572                    self.fb_addr,
2573                    self.fb_addr.add(pixels * self.pitch),
2574                    move_rows * self.pitch,
2575                );
2576            }
2577        }
2578        self.fill_rect(0, 0, text_w, pixels, self.unpack_color(self.bg));
2579    }
2580
2581    /// Performs the layout text lines operation.
2582    fn layout_text_lines(&self, text: &str, wrap: bool, max_cols: Option<usize>) -> Vec<Vec<char>> {
2583        let mut lines: Vec<Vec<char>> = Vec::new();
2584        let mut current: Vec<char> = Vec::new();
2585        let wrap_cols = max_cols.filter(|&c| c > 0);
2586
2587        for ch in text.chars() {
2588            if ch == '\n' {
2589                lines.push(current);
2590                current = Vec::new();
2591                continue;
2592            }
2593
2594            if wrap {
2595                if let Some(cols) = wrap_cols {
2596                    if current.len() >= cols {
2597                        lines.push(current);
2598                        current = Vec::new();
2599                    }
2600                }
2601            }
2602
2603            current.push(ch);
2604        }
2605
2606        lines.push(current);
2607        lines
2608    }
2609
2610    /// Performs the measure text operation.
2611    pub fn measure_text(&self, text: &str, max_width: Option<usize>, wrap: bool) -> TextMetrics {
2612        if !self.enabled {
2613            return TextMetrics {
2614                width: 0,
2615                height: 0,
2616                lines: 0,
2617            };
2618        }
2619        let gw = self.font_info.glyph_w;
2620        let gh = self.font_info.glyph_h;
2621        let max_cols = max_width.map(|w| core::cmp::max(1, w / gw));
2622        let lines = self.layout_text_lines(text, wrap, max_cols);
2623
2624        let mut max_line_cols = 0usize;
2625        for line in &lines {
2626            max_line_cols = core::cmp::max(max_line_cols, line.len());
2627        }
2628
2629        TextMetrics {
2630            width: max_line_cols * gw,
2631            height: lines.len() * gh,
2632            lines: lines.len(),
2633        }
2634    }
2635
2636    /// Performs the draw text operation.
2637    pub fn draw_text(
2638        &mut self,
2639        pixel_x: usize,
2640        pixel_y: usize,
2641        text: &str,
2642        opts: TextOptions,
2643    ) -> TextMetrics {
2644        if !self.enabled {
2645            return TextMetrics {
2646                width: 0,
2647                height: 0,
2648                lines: 0,
2649            };
2650        }
2651
2652        let gw = self.font_info.glyph_w;
2653        let gh = self.font_info.glyph_h;
2654        let max_cols = opts.max_width.map(|w| core::cmp::max(1, w / gw));
2655        let lines = self.layout_text_lines(text, opts.wrap, max_cols);
2656        let region_w = opts.max_width.unwrap_or_else(|| {
2657            let mut max_line_cols = 0usize;
2658            for line in &lines {
2659                max_line_cols = core::cmp::max(max_line_cols, line.len());
2660            }
2661            max_line_cols * gw
2662        });
2663
2664        let fg = self.pack_color(opts.fg);
2665        let bg = self.pack_color(opts.bg);
2666        let mut max_line_px = 0usize;
2667
2668        for (line_idx, line) in lines.iter().enumerate() {
2669            let line_px = line.len() * gw;
2670            max_line_px = core::cmp::max(max_line_px, line_px);
2671            let x = match opts.align {
2672                TextAlign::Left => pixel_x,
2673                TextAlign::Center => pixel_x.saturating_add(region_w.saturating_sub(line_px) / 2),
2674                TextAlign::Right => pixel_x.saturating_add(region_w.saturating_sub(line_px)),
2675            };
2676            let y = pixel_y + line_idx * gh;
2677
2678            for (col, ch) in line.iter().enumerate() {
2679                self.draw_glyph_at_pixel(x + col * gw, y, *ch, fg, bg);
2680            }
2681        }
2682
2683        TextMetrics {
2684            width: max_line_px,
2685            height: lines.len() * gh,
2686            lines: lines.len(),
2687        }
2688    }
2689
2690    /// Performs the draw strata stack operation.
2691    pub fn draw_strata_stack(
2692        &mut self,
2693        origin_x: usize,
2694        origin_y: usize,
2695        layer_w: usize,
2696        layer_h: usize,
2697    ) {
2698        if !self.enabled || layer_w == 0 || layer_h == 0 {
2699            return;
2700        }
2701
2702        // Simple "strata" stack: each layer is slightly shifted and tinted.
2703        let palette = [
2704            RgbColor::new(0x24, 0x3B, 0x55),
2705            RgbColor::new(0x2B, 0x54, 0x77),
2706            RgbColor::new(0x2F, 0x74, 0x93),
2707            RgbColor::new(0x3A, 0x93, 0xA8),
2708            RgbColor::new(0x5F, 0xB1, 0xA1),
2709            RgbColor::new(0xA4, 0xCC, 0x94),
2710        ];
2711
2712        let dx = 6usize;
2713        let dy = 5usize;
2714        for (i, color) in palette.iter().enumerate() {
2715            let x = origin_x.saturating_add(i * dx);
2716            let y = origin_y.saturating_add(i * dy);
2717            let w = layer_w.saturating_sub(i * dx);
2718            let h = layer_h.saturating_sub(i * dy);
2719            if w < 8 || h < 8 {
2720                break;
2721            }
2722
2723            self.fill_rect(x, y, w, h, *color);
2724            self.draw_rect(x, y, w, h, RgbColor::new(0x10, 0x16, 0x20));
2725        }
2726    }
2727
2728    /// Performs the draw glyph operation.
2729    fn draw_glyph(&mut self, cx: usize, cy: usize, ch: char) {
2730        let glyph_index = self.glyph_index_for_char(ch);
2731        self.draw_glyph_index_at_pixel(
2732            cx * self.font_info.glyph_w,
2733            cy * self.font_info.glyph_h,
2734            glyph_index,
2735            self.fg,
2736            self.bg,
2737        );
2738    }
2739
2740    /// Performs the clear row operation.
2741    fn clear_row(&mut self, row: usize) {
2742        if !self.enabled {
2743            return;
2744        }
2745        self.clear_text_line_pixels(row);
2746    }
2747
2748    /// Performs the scroll operation.
2749    fn scroll(&mut self) {
2750        if !self.enabled {
2751            return;
2752        }
2753        let dy = self.font_info.glyph_h;
2754        let text_h = self.text_area_height();
2755        if dy >= text_h {
2756            self.clear();
2757            return;
2758        }
2759        self.move_text_view_pixels_up(dy);
2760        self.row = self.rows - 1;
2761    }
2762
2763    /// Writes char.
2764    fn write_char(&mut self, c: char) {
2765        if !self.enabled {
2766            return;
2767        }
2768        let c = normalize_console_char(c);
2769
2770        // Mirror into scrollback (always, even when scrolled back) ========================================
2771        self.sb_mirror_char(c);
2772        // If the user is viewing history, suppress live rendering.
2773        if self.scroll_offset > 0 {
2774            return;
2775        }
2776        // ==============================
2777
2778        match c {
2779            '\n' => {
2780                self.col = 0;
2781                self.row += 1;
2782            }
2783            '\r' => self.col = 0,
2784            '\t' => self.col = (self.col + 4) & !3,
2785            '\u{8}' => {
2786                if self.col > 0 {
2787                    self.col -= 1;
2788                    self.draw_glyph(self.col, self.row, ' ');
2789                }
2790            }
2791            '\0' => {}
2792            ch => {
2793                self.draw_glyph(self.col, self.row, ch);
2794                self.col += 1;
2795            }
2796        }
2797
2798        if self.col >= self.cols {
2799            self.col = 0;
2800            self.row += 1;
2801        }
2802
2803        if self.row >= self.rows {
2804            self.scroll();
2805        }
2806    }
2807
2808    /// Writes bytes.
2809    fn write_bytes(&mut self, s: &str) {
2810        let (prev_draw_to_back, prev_track_dirty) = self.begin_viewport_render();
2811        // Skip basic ANSI escape sequences to avoid rendering control garbage.
2812        let mut chars = s.chars();
2813        while let Some(ch) = chars.next() {
2814            if ch == '\u{1b}' {
2815                if matches!(chars.clone().next(), Some('[')) {
2816                    let _ = chars.next();
2817                    for c in chars.by_ref() {
2818                        if ('@'..='~').contains(&c) {
2819                            break;
2820                        }
2821                    }
2822                }
2823                continue;
2824            }
2825            self.write_char(ch);
2826        }
2827        // Refresh scrollbar after each batch of output.
2828        self.draw_scrollbar_inner();
2829        self.end_viewport_render(prev_draw_to_back, prev_track_dirty);
2830    }
2831
2832    // ═══════════════════════════════════════════════════════════════════
2833    // Scrollback buffer + scrollbar
2834    // ═══════════════════════════════════════════════════════════════════
2835
2836    /// Mirror a normalized character into the scrollback model.
2837    /// Called by `write_char` before any live rendering.
2838    fn sb_mirror_char(&mut self, c: char) {
2839        let cols = self.cols;
2840        let fg = self.fg;
2841        let bg = self.bg;
2842        match c {
2843            '\n' => {
2844                let row = core::mem::take(&mut self.sb_cur_row);
2845                self.sb_push_row(row);
2846                self.sb_trim();
2847            }
2848            '\r' => {
2849                self.sb_cur_row.clear();
2850            }
2851            '\t' => {
2852                let stop = (self.sb_cur_row.len() + 4) & !3;
2853                let end = stop.min(cols);
2854                while self.sb_cur_row.len() < end {
2855                    self.sb_cur_row.push(SbCell { ch: ' ', fg, bg });
2856                }
2857                if self.sb_cur_row.len() >= cols {
2858                    let row = core::mem::take(&mut self.sb_cur_row);
2859                    self.sb_push_row(row);
2860                    self.sb_trim();
2861                }
2862            }
2863            '\u{8}' => {
2864                self.sb_cur_row.pop();
2865            }
2866            '\0' => {}
2867            ch => {
2868                self.sb_cur_row.push(SbCell { ch, fg, bg });
2869                if self.sb_cur_row.len() >= cols {
2870                    let row = core::mem::take(&mut self.sb_cur_row);
2871                    self.sb_push_row(row);
2872                    self.sb_trim();
2873                }
2874            }
2875        }
2876    }
2877
2878    /// Keep the scrollback buffer within MAX_SCROLLBACK + rows.
2879    #[inline]
2880    fn sb_trim(&mut self) {
2881        let cap = self.sb_capacity();
2882        if self.sb_rows.len() <= cap {
2883            return;
2884        }
2885        let keep = cap.min(self.sb_rows.len());
2886        let start = self.sb_rows.len().saturating_sub(keep);
2887        let mut compacted = Vec::with_capacity(keep);
2888        for idx in start..self.sb_rows.len() {
2889            if let Some(row) = self.sb_row_at(idx).cloned() {
2890                compacted.push(row);
2891            }
2892        }
2893        self.sb_rows = compacted;
2894        self.sb_row_head = 0;
2895    }
2896
2897    /// Draw (or refresh) the scrollbar strip on the right edge of the text area.
2898    fn draw_scrollbar_inner(&mut self) {
2899        if !self.enabled || self.fb_width == 0 {
2900            return;
2901        }
2902        let text_h = self.text_area_height();
2903        if text_h == 0 {
2904            return;
2905        }
2906        let sb_x = self.fb_width.saturating_sub(SCROLLBAR_W);
2907        let total = self.sb_rows.len() + 1; // +1 accounts for current partial row
2908        let track_packed = self.fmt.pack_rgb(0x22, 0x28, 0x38);
2909        let thumb_packed = self.fmt.pack_rgb(0x58, 0x72, 0xA0);
2910        let thumb_hi = self.fmt.pack_rgb(0x80, 0xA0, 0xC8);
2911
2912        if total <= self.rows {
2913            // Not enough content to scroll: full-height thumb.
2914            for y in 0..text_h {
2915                for x in sb_x..self.fb_width {
2916                    let c = if x == sb_x || x == self.fb_width - 1 || y == 0 || y == text_h - 1 {
2917                        track_packed
2918                    } else {
2919                        thumb_hi
2920                    };
2921                    self.put_pixel_raw(x, y, c);
2922                }
2923            }
2924            return;
2925        }
2926
2927        let max_offset = total.saturating_sub(self.rows);
2928        let thumb_h = ((text_h * self.rows) / total).max(6);
2929        let avail = text_h.saturating_sub(thumb_h);
2930        // offset 0 = thumb at bottom; max_offset = thumb at top
2931        let thumb_y = if self.scroll_offset == 0 || avail == 0 {
2932            avail // = text_h - thumb_h (bottom)
2933        } else {
2934            avail - (avail * self.scroll_offset / max_offset)
2935        };
2936
2937        for y in 0..text_h {
2938            let in_thumb = y >= thumb_y && y < thumb_y + thumb_h;
2939            let packed = if in_thumb { thumb_packed } else { track_packed };
2940            for x in sb_x..self.fb_width {
2941                self.put_pixel_raw(x, y, packed);
2942            }
2943        }
2944    }
2945
2946    /// Redraw the entire text area from the scrollback buffer.
2947    /// Called when scroll_offset changes.
2948    fn redraw_from_scrollback(&mut self) {
2949        if !self.enabled {
2950            return;
2951        }
2952        self.render_viewport_full();
2953    }
2954
2955    fn redraw_from_scrollback_incremental(&mut self, old_offset: usize) -> bool {
2956        if !self.enabled || self.rows == 0 {
2957            return false;
2958        }
2959        let diff = old_offset.abs_diff(self.scroll_offset);
2960        if diff == 0 || diff >= self.rows {
2961            return false;
2962        }
2963        let pixel_delta = diff.saturating_mul(self.font_info.glyph_h);
2964        if pixel_delta == 0 {
2965            return false;
2966        }
2967
2968        if self.scroll_offset > old_offset {
2969            self.move_text_view_pixels_down(pixel_delta);
2970            self.redraw_visible_rows(0, diff);
2971        } else {
2972            self.move_text_view_pixels_up(pixel_delta);
2973            self.redraw_visible_rows(self.rows.saturating_sub(diff), diff);
2974        }
2975
2976        self.finalize_live_view_state();
2977        self.draw_scrollbar_inner();
2978        self.request_present();
2979        true
2980    }
2981
2982    /// Scroll the view up (backward in history) by `lines` lines.
2983    pub fn scroll_view_up(&mut self, lines: usize) {
2984        if !self.enabled {
2985            return;
2986        }
2987        let total = self.sb_rows.len() + 1;
2988        let max_off = total.saturating_sub(self.rows);
2989        self.set_scroll_offset_and_render((self.scroll_offset + lines).min(max_off));
2990    }
2991
2992    /// Scroll the view down (forward, toward live) by `lines` lines.
2993    pub fn scroll_view_down(&mut self, lines: usize) {
2994        if !self.enabled {
2995            return;
2996        }
2997        self.set_scroll_offset_and_render(self.scroll_offset.saturating_sub(lines));
2998    }
2999
3000    /// Immediately return to the live (bottom) view.
3001    pub fn scroll_to_live(&mut self) {
3002        if self.scroll_offset == 0 {
3003            return;
3004        }
3005        self.set_scroll_offset_and_render(0);
3006    }
3007
3008    /// Handle a click at pixel `(px_x, px_y)` : if it falls in the scrollbar,
3009    /// jump the view to the corresponding scroll position.
3010    pub fn scrollbar_click(&mut self, px_x: usize, px_y: usize) {
3011        if !self.enabled {
3012            return;
3013        }
3014        let sb_x = self.fb_width.saturating_sub(SCROLLBAR_W);
3015        if px_x < sb_x {
3016            return;
3017        }
3018        let text_h = self.text_area_height();
3019        if text_h <= 1 {
3020            return;
3021        }
3022        let total = self.sb_rows.len() + 1;
3023        let max_off = total.saturating_sub(self.rows);
3024        if max_off == 0 {
3025            return;
3026        }
3027        // py = 0 → top = oldest = max_offset; py = text_h - 1 → bottom = 0
3028        let py = px_y.min(text_h - 1);
3029        let offset = max_off * (text_h - 1 - py) / (text_h - 1);
3030        self.set_scroll_offset_and_render(offset.min(max_off));
3031    }
3032
3033    /// Drag the scrollbar thumb to vertical pixel `px_y`.
3034    ///
3035    /// Unlike `scrollbar_click`, this only depends on Y and is intended for
3036    /// click-and-drag interactions where the pointer may slightly leave the
3037    /// scrollbar strip horizontally.
3038    pub fn scrollbar_drag_to(&mut self, px_y: usize) {
3039        if !self.enabled {
3040            return;
3041        }
3042        let text_h = self.text_area_height();
3043        if text_h <= 1 {
3044            return;
3045        }
3046        let total = self.sb_rows.len() + 1;
3047        let max_off = total.saturating_sub(self.rows);
3048        if max_off == 0 {
3049            return;
3050        }
3051        // py = 0 -> top = oldest = max_offset; py = text_h - 1 -> bottom = 0
3052        let py = px_y.min(text_h - 1);
3053        let offset = max_off * (text_h - 1 - py) / (text_h - 1);
3054        self.set_scroll_offset_and_render(offset.min(max_off));
3055    }
3056
3057    /// Returns `true` if the pixel coordinates fall within the scrollbar strip.
3058    pub fn scrollbar_hit_test(&self, px_x: usize, px_y: usize) -> bool {
3059        if !self.enabled {
3060            return false;
3061        }
3062        let sb_x = self.fb_width.saturating_sub(SCROLLBAR_W);
3063        px_x >= sb_x && px_y < self.text_area_height()
3064    }
3065}
3066
3067/// Performs the normalize console char operation.
3068fn normalize_console_char(ch: char) -> char {
3069    match ch {
3070        '\n' | '\r' | '\t' | '\u{8}' => ch,
3071        c if c.is_control() => '\0',
3072        // Graceful fallback when font lacks box-drawing coverage.
3073        '\u{2500}' | '\u{2501}' | '\u{2504}' | '\u{2505}' | '\u{2013}' | '\u{2014}' => '-',
3074        '\u{2502}' | '\u{2503}' => '|',
3075        '\u{250c}' | '\u{2510}' | '\u{2514}' | '\u{2518}' | '\u{251c}' | '\u{2524}'
3076        | '\u{252c}' | '\u{2534}' | '\u{253c}' => '+',
3077        '\u{00a0}' => ' ',
3078        _ => ch,
3079    }
3080}
3081
3082impl fmt::Write for VgaWriter {
3083    /// Writes str.
3084    fn write_str(&mut self, s: &str) -> fmt::Result {
3085        if self.enabled {
3086            self.write_bytes(s);
3087        } else {
3088            crate::arch::x86_64::serial::_print(format_args!("{}", s));
3089        }
3090        Ok(())
3091    }
3092}
3093
3094pub static VGA_WRITER: Mutex<VgaWriter> = Mutex::new(VgaWriter::new());
3095
3096/// Returns whether available.
3097#[inline]
3098pub fn is_available() -> bool {
3099    VGA_AVAILABLE.load(Ordering::Relaxed)
3100}
3101
3102/// Performs the with writer operation.
3103pub fn with_writer<R>(f: impl FnOnce(&mut VgaWriter) -> R) -> Option<R> {
3104    if !is_available() {
3105        return None;
3106    }
3107    let mut writer = VGA_WRITER.lock();
3108    Some(f(&mut writer))
3109}
3110
3111/// Writes raw console text to the framebuffer console in a single writer batch.
3112pub fn write_text(text: &str) {
3113    if !is_available() {
3114        crate::arch::x86_64::serial::_print(format_args!("{}", text));
3115        return;
3116    }
3117    let mut writer = VGA_WRITER.lock();
3118    writer.write_bytes(text);
3119}
3120
3121/// Writes one console character to the framebuffer console.
3122pub fn write_char(ch: char) {
3123    if !is_available() {
3124        crate::arch::x86_64::serial::_print(format_args!("{}", ch));
3125        return;
3126    }
3127    let mut buf = [0u8; 4];
3128    write_text(ch.encode_utf8(&mut buf));
3129}
3130
3131/// Performs the status line info operation.
3132fn status_line_info() -> StatusLineInfo {
3133    let mut guard = STATUS_LINE_INFO.lock();
3134    if guard.is_none() {
3135        *guard = Some(StatusLineInfo {
3136            hostname: String::from("strat9"),
3137            ip: String::from("n/a"),
3138        });
3139    }
3140    guard.as_ref().cloned().unwrap_or(StatusLineInfo {
3141        hostname: String::from("strat9"),
3142        ip: String::from("n/a"),
3143    })
3144}
3145
3146/// Performs the format uptime from ticks operation.
3147fn format_uptime_from_ticks(ticks: u64) -> String {
3148    let total_secs = ticks / 100;
3149    let h = total_secs / 3600;
3150    let m = (total_secs % 3600) / 60;
3151    let s = total_secs % 60;
3152    format!("{:02}:{:02}:{:02}", h, m, s)
3153}
3154
3155/// Performs the current fps operation.
3156fn current_fps(tick: u64) -> u64 {
3157    let last_tick = FPS_LAST_TICK.load(Ordering::Relaxed);
3158    let frames = PRESENTED_FRAMES.load(Ordering::Relaxed);
3159
3160    if last_tick == 0 {
3161        let _ = FPS_LAST_TICK.compare_exchange(0, tick, Ordering::Relaxed, Ordering::Relaxed);
3162        let _ =
3163            FPS_LAST_FRAME_COUNT.compare_exchange(0, frames, Ordering::Relaxed, Ordering::Relaxed);
3164        return FPS_ESTIMATE.load(Ordering::Relaxed);
3165    }
3166
3167    let dt = tick.saturating_sub(last_tick);
3168    if dt >= STATUS_REFRESH_PERIOD_TICKS
3169        && FPS_LAST_TICK
3170            .compare_exchange(last_tick, tick, Ordering::Relaxed, Ordering::Relaxed)
3171            .is_ok()
3172    {
3173        let last_frames = FPS_LAST_FRAME_COUNT.swap(frames, Ordering::Relaxed);
3174        let df = frames.saturating_sub(last_frames);
3175        let fps = if dt == 0 {
3176            0
3177        } else {
3178            df.saturating_mul(100) / dt
3179        };
3180        FPS_ESTIMATE.store(fps, Ordering::Relaxed);
3181    }
3182
3183    FPS_ESTIMATE.load(Ordering::Relaxed)
3184}
3185
3186/// Performs the current ui scale operation.
3187fn current_ui_scale() -> UiScale {
3188    match UI_SCALE.load(Ordering::Relaxed) {
3189        1 => UiScale::Compact,
3190        3 => UiScale::Large,
3191        _ => UiScale::Normal,
3192    }
3193}
3194
3195/// Performs the ui scale operation.
3196pub fn ui_scale() -> UiScale {
3197    current_ui_scale()
3198}
3199
3200/// Sets ui scale.
3201pub fn set_ui_scale(scale: UiScale) {
3202    UI_SCALE.store(scale as u8, Ordering::Relaxed);
3203}
3204
3205/// Performs the ui scale px operation.
3206pub fn ui_scale_px(base: usize) -> usize {
3207    let factor = current_ui_scale().factor();
3208    let denom = UiScale::Normal.factor();
3209    base.saturating_mul(factor) / denom
3210}
3211
3212/// Performs the format mem usage operation.
3213fn format_mem_usage() -> String {
3214    let lock = crate::memory::buddy::get_allocator();
3215    let Some(guard) = lock.try_lock() else {
3216        // Never block the status-line task on allocator contention.
3217        return String::from("n/a");
3218    };
3219    let Some(alloc) = guard.as_ref() else {
3220        return String::from("n/a");
3221    };
3222    let (total_pages, allocated_pages) = alloc.page_totals();
3223    let page_size = 4096usize;
3224    let total = total_pages.saturating_mul(page_size);
3225    let used = allocated_pages.saturating_mul(page_size);
3226    let free = total.saturating_sub(used);
3227    format!("{}/{}", format_size(free), format_size(total))
3228}
3229
3230/// Performs the format size operation.
3231fn format_size(bytes: usize) -> String {
3232    const KB: usize = 1024;
3233    const MB: usize = 1024 * KB;
3234    const GB: usize = 1024 * MB;
3235    if bytes >= GB {
3236        format!("{}G", bytes / GB)
3237    } else if bytes >= MB {
3238        format!("{}M", bytes / MB)
3239    } else if bytes >= KB {
3240        format!("{}K", bytes / KB)
3241    } else {
3242        format!("{}B", bytes)
3243    }
3244}
3245
3246/// Performs the draw status bar inner operation.
3247fn draw_status_bar_inner(w: &mut VgaWriter, left: &str, right: &str, theme: UiTheme) {
3248    let saved_clip = w.clip;
3249    w.reset_clip_rect();
3250
3251    let (gw, gh) = w.glyph_size();
3252    if gh == 0 || gw == 0 {
3253        w.clip = saved_clip;
3254        return;
3255    }
3256    let bar_h = gh;
3257    let y = w.height().saturating_sub(bar_h);
3258    w.fill_rect(0, y, w.width(), bar_h, theme.status_bg);
3259
3260    let left_opts = TextOptions {
3261        fg: theme.status_text,
3262        bg: theme.status_bg,
3263        align: TextAlign::Left,
3264        wrap: false,
3265        max_width: Some(w.width().saturating_sub(8)),
3266    };
3267    w.draw_text(0, y, left, left_opts);
3268
3269    let right_opts = TextOptions {
3270        fg: theme.status_text,
3271        bg: theme.status_bg,
3272        align: TextAlign::Right,
3273        wrap: false,
3274        max_width: Some(w.width()),
3275    };
3276    w.draw_text(0, y, right, right_opts);
3277    w.clip = saved_clip;
3278}
3279
3280/// Performs the init operation.
3281#[allow(clippy::too_many_arguments)]
3282pub fn init(
3283    fb_addr: u64,
3284    fb_width: u32,
3285    fb_height: u32,
3286    pitch: u32,
3287    bpp: u16,
3288    red_size: u8,
3289    red_shift: u8,
3290    green_size: u8,
3291    green_shift: u8,
3292    blue_size: u8,
3293    blue_shift: u8,
3294) {
3295    if fb_addr == 0 || fb_width == 0 || fb_height == 0 || pitch == 0 {
3296        VGA_AVAILABLE.store(false, Ordering::Relaxed);
3297        log::info!("Framebuffer console unavailable (no framebuffer)");
3298        return;
3299    }
3300
3301    if bpp != 24 && bpp != 32 {
3302        VGA_AVAILABLE.store(false, Ordering::Relaxed);
3303        log::info!("Framebuffer console unavailable (unsupported bpp={})", bpp);
3304        return;
3305    }
3306
3307    let fmt = PixelFormat {
3308        bpp,
3309        red_size,
3310        red_shift,
3311        green_size,
3312        green_shift,
3313        blue_size,
3314        blue_shift,
3315    };
3316
3317    // Ensure fb_addr is a virtual address in the HHDM.
3318    // If it's already higher-half (>= HHDM), use it as-is.
3319    // Otherwise, convert it via phys_to_virt.
3320    //
3321    // This fix works on VMWare Workstation
3322    //
3323    let hhdm = crate::memory::hhdm_offset();
3324    let fb_virt = if hhdm != 0 && fb_addr < hhdm {
3325        crate::memory::phys_to_virt(fb_addr)
3326    } else {
3327        fb_addr
3328    };
3329
3330    let mut writer = VGA_WRITER.lock();
3331    if writer.configure(
3332        fb_virt as *mut u8,
3333        fb_width as usize,
3334        fb_height as usize,
3335        pitch as usize,
3336        fmt,
3337    ) {
3338        writer.set_color(Color::LightCyan, Color::Black);
3339        writer.clear_with(RgbColor::new(0x12, 0x16, 0x1E));
3340        // Decorative background mark for Strat9 identity.
3341        let deco_w = (writer.width() / 3).clamp(120, 300);
3342        let deco_h = (writer.height() / 4).clamp(90, 220);
3343        let deco_x = writer.width().saturating_sub(deco_w + 24);
3344        let deco_y = 24;
3345        writer.draw_strata_stack(deco_x, deco_y, deco_w, deco_h);
3346        writer.set_rgb_color(
3347            RgbColor::new(0xA7, 0xD8, 0xD8),
3348            RgbColor::new(0x12, 0x16, 0x1E),
3349        );
3350        writer.write_bytes("Strat9-OS v0.1.0\n");
3351        writer.set_rgb_color(
3352            RgbColor::new(0xE2, 0xE8, 0xF0),
3353            RgbColor::new(0x12, 0x16, 0x1E),
3354        );
3355        VGA_AVAILABLE.store(true, Ordering::Relaxed);
3356        log::info!(
3357            "Framebuffer console enabled: {}x{} {}bpp pitch={}",
3358            fb_width,
3359            fb_height,
3360            bpp,
3361            pitch
3362        );
3363        drop(writer);
3364        draw_boot_status_line(UiTheme::OCEAN_STATUS);
3365    } else {
3366        writer.enabled = false;
3367        VGA_AVAILABLE.store(false, Ordering::Relaxed);
3368        log::info!("Framebuffer console unavailable (font parse/init failed)");
3369    }
3370}
3371
3372/// Print to framebuffer console (falls back to serial when unavailable).
3373#[macro_export]
3374macro_rules! vga_print {
3375    ($($arg:tt)*) => {
3376        $crate::arch::x86_64::vga::_print(format_args!($($arg)*));
3377    };
3378}
3379
3380/// Print line to framebuffer console (falls back to serial when unavailable).
3381#[macro_export]
3382macro_rules! vga_println {
3383    () => ($crate::vga_print!("\n"));
3384    ($($arg:tt)*) => ($crate::vga_print!("{}\n", format_args!($($arg)*)));
3385}
3386
3387/// Performs the print operation.
3388#[doc(hidden)]
3389pub fn _print(args: fmt::Arguments) {
3390    use core::fmt::Write;
3391    if is_available() {
3392        VGA_WRITER.lock().write_fmt(args).ok();
3393        return;
3394    }
3395    crate::arch::x86_64::serial::_print(args);
3396}
3397
3398#[derive(Debug, Clone, Copy)]
3399pub struct Canvas {
3400    fg: RgbColor,
3401    bg: RgbColor,
3402}
3403
3404impl Default for Canvas {
3405    /// Builds a default instance.
3406    fn default() -> Self {
3407        Self {
3408            fg: RgbColor::LIGHT_GREY,
3409            bg: RgbColor::BLACK,
3410        }
3411    }
3412}
3413
3414impl Canvas {
3415    /// Creates a new instance.
3416    pub const fn new(fg: RgbColor, bg: RgbColor) -> Self {
3417        Self { fg, bg }
3418    }
3419
3420    /// Sets fg.
3421    pub fn set_fg(&mut self, fg: RgbColor) {
3422        self.fg = fg;
3423    }
3424
3425    /// Sets bg.
3426    pub fn set_bg(&mut self, bg: RgbColor) {
3427        self.bg = bg;
3428    }
3429
3430    /// Sets colors.
3431    pub fn set_colors(&mut self, fg: RgbColor, bg: RgbColor) {
3432        self.fg = fg;
3433        self.bg = bg;
3434    }
3435
3436    /// Sets clip rect.
3437    pub fn set_clip_rect(&self, x: usize, y: usize, w: usize, h: usize) {
3438        set_clip_rect(x, y, w, h);
3439    }
3440
3441    /// Performs the reset clip rect operation.
3442    pub fn reset_clip_rect(&self) {
3443        reset_clip_rect();
3444    }
3445
3446    /// Performs the clear operation.
3447    pub fn clear(&self) {
3448        fill_rect(0, 0, width(), height(), self.bg);
3449    }
3450
3451    /// Performs the pixel operation.
3452    pub fn pixel(&self, x: usize, y: usize) {
3453        draw_pixel(x, y, self.fg);
3454    }
3455
3456    /// Performs the line operation.
3457    pub fn line(&self, x0: isize, y0: isize, x1: isize, y1: isize) {
3458        draw_line(x0, y0, x1, y1, self.fg);
3459    }
3460
3461    /// Performs the rect operation.
3462    pub fn rect(&self, x: usize, y: usize, w: usize, h: usize) {
3463        draw_rect(x, y, w, h, self.fg);
3464    }
3465
3466    /// Performs the fill rect operation.
3467    pub fn fill_rect(&self, x: usize, y: usize, w: usize, h: usize) {
3468        fill_rect(x, y, w, h, self.fg);
3469    }
3470
3471    /// Performs the fill rect alpha operation.
3472    pub fn fill_rect_alpha(&self, x: usize, y: usize, w: usize, h: usize, alpha: u8) {
3473        fill_rect_alpha(x, y, w, h, self.fg, alpha);
3474    }
3475
3476    /// Performs the text operation.
3477    pub fn text(&self, x: usize, y: usize, text: &str) {
3478        draw_text_at(x, y, text, self.fg, self.bg);
3479    }
3480
3481    /// Performs the text opts operation.
3482    pub fn text_opts(
3483        &self,
3484        x: usize,
3485        y: usize,
3486        text: &str,
3487        align: TextAlign,
3488        wrap: bool,
3489        max_width: Option<usize>,
3490    ) -> TextMetrics {
3491        draw_text(
3492            x,
3493            y,
3494            text,
3495            TextOptions {
3496                fg: self.fg,
3497                bg: self.bg,
3498                align,
3499                wrap,
3500                max_width,
3501            },
3502        )
3503    }
3504
3505    /// Performs the measure text operation.
3506    pub fn measure_text(&self, text: &str, max_width: Option<usize>, wrap: bool) -> TextMetrics {
3507        measure_text(text, max_width, wrap)
3508    }
3509
3510    /// Performs the blit rgb operation.
3511    pub fn blit_rgb(&self, x: usize, y: usize, w: usize, h: usize, pixels: &[RgbColor]) -> bool {
3512        blit_rgb(x, y, w, h, pixels)
3513    }
3514
3515    /// Performs the blit rgb24 operation.
3516    pub fn blit_rgb24(&self, x: usize, y: usize, w: usize, h: usize, bytes: &[u8]) -> bool {
3517        blit_rgb24(x, y, w, h, bytes)
3518    }
3519
3520    /// Performs the blit rgba operation.
3521    pub fn blit_rgba(
3522        &self,
3523        x: usize,
3524        y: usize,
3525        w: usize,
3526        h: usize,
3527        bytes: &[u8],
3528        global_alpha: u8,
3529    ) -> bool {
3530        blit_rgba(x, y, w, h, bytes, global_alpha)
3531    }
3532
3533    /// Performs the blit sprite rgba operation.
3534    pub fn blit_sprite_rgba(
3535        &self,
3536        x: usize,
3537        y: usize,
3538        sprite: SpriteRgba<'_>,
3539        global_alpha: u8,
3540    ) -> bool {
3541        blit_sprite_rgba(x, y, sprite, global_alpha)
3542    }
3543
3544    /// Performs the begin frame operation.
3545    pub fn begin_frame(&self) -> bool {
3546        begin_frame()
3547    }
3548
3549    /// Performs the end frame operation.
3550    pub fn end_frame(&self) {
3551        end_frame();
3552    }
3553
3554    /// Performs the ui clear operation.
3555    pub fn ui_clear(&self, theme: UiTheme) {
3556        ui_clear(theme);
3557    }
3558
3559    /// Performs the ui panel operation.
3560    pub fn ui_panel(
3561        &self,
3562        x: usize,
3563        y: usize,
3564        w: usize,
3565        h: usize,
3566        title: &str,
3567        body: &str,
3568        theme: UiTheme,
3569    ) {
3570        ui_draw_panel(x, y, w, h, title, body, theme);
3571    }
3572
3573    /// Performs the ui status bar operation.
3574    pub fn ui_status_bar(&self, left: &str, right: &str, theme: UiTheme) {
3575        ui_draw_status_bar(left, right, theme);
3576    }
3577
3578    /// Performs the system status line operation.
3579    pub fn system_status_line(&self, theme: UiTheme) {
3580        draw_system_status_line(theme);
3581    }
3582
3583    /// Performs the layout screen operation.
3584    pub fn layout_screen(&self) -> UiDockLayout {
3585        UiDockLayout::from_screen()
3586    }
3587
3588    /// Performs the ui label operation.
3589    pub fn ui_label(&self, label: &UiLabel<'_>) {
3590        ui_draw_label(label);
3591    }
3592
3593    /// Performs the ui progress bar operation.
3594    pub fn ui_progress_bar(&self, bar: UiProgressBar) {
3595        ui_draw_progress_bar(bar);
3596    }
3597
3598    /// Performs the ui table operation.
3599    pub fn ui_table(&self, table: &UiTable) {
3600        ui_draw_table(table);
3601    }
3602}
3603
3604/// Performs the width operation.
3605pub fn width() -> usize {
3606    if !is_available() {
3607        return 0;
3608    }
3609    VGA_WRITER.lock().width()
3610}
3611
3612/// Performs the height operation.
3613pub fn height() -> usize {
3614    if !is_available() {
3615        return 0;
3616    }
3617    VGA_WRITER.lock().height()
3618}
3619
3620/// Performs the screen size operation.
3621pub fn screen_size() -> (usize, usize) {
3622    (width(), height())
3623}
3624
3625/// Performs the ui layout screen operation.
3626pub fn ui_layout_screen() -> UiDockLayout {
3627    UiDockLayout::from_screen()
3628}
3629
3630/// Performs the glyph size operation.
3631pub fn glyph_size() -> (usize, usize) {
3632    if !is_available() {
3633        return (0, 0);
3634    }
3635    VGA_WRITER.lock().glyph_size()
3636}
3637
3638/// Performs the text cols operation.
3639pub fn text_cols() -> usize {
3640    if !is_available() {
3641        return 0;
3642    }
3643    VGA_WRITER.lock().cols()
3644}
3645
3646/// Performs the text rows operation.
3647pub fn text_rows() -> usize {
3648    if !is_available() {
3649        return 0;
3650    }
3651    VGA_WRITER.lock().rows()
3652}
3653
3654/// Returns text cursor.
3655pub fn get_text_cursor() -> (usize, usize) {
3656    if !is_available() {
3657        return (0, 0);
3658    }
3659    let writer = VGA_WRITER.lock();
3660    (writer.col, writer.row)
3661}
3662
3663/// Sets text cursor.
3664pub fn set_text_cursor(col: usize, row: usize) {
3665    if !is_available() {
3666        return;
3667    }
3668    VGA_WRITER.lock().set_cursor_cell(col, row);
3669}
3670
3671/// Performs the double buffer mode operation.
3672pub fn double_buffer_mode() -> bool {
3673    DOUBLE_BUFFER_MODE.load(Ordering::Relaxed)
3674}
3675
3676/// Sets double buffer mode.
3677pub fn set_double_buffer_mode(enabled: bool) {
3678    DOUBLE_BUFFER_MODE.store(enabled, Ordering::Relaxed);
3679}
3680
3681/// Performs the draw text cursor operation.
3682pub fn draw_text_cursor(color: RgbColor) {
3683    if !is_available() {
3684        return;
3685    }
3686    let mut writer = VGA_WRITER.lock();
3687    writer.draw_text_cursor_overlay(color);
3688}
3689
3690/// Performs the framebuffer info operation.
3691pub fn framebuffer_info() -> FramebufferInfo {
3692    if !is_available() {
3693        return FramebufferInfo {
3694            available: false,
3695            width: 0,
3696            height: 0,
3697            pitch: 0,
3698            bpp: 0,
3699            red_size: 0,
3700            red_shift: 0,
3701            green_size: 0,
3702            green_shift: 0,
3703            blue_size: 0,
3704            blue_shift: 0,
3705            text_cols: 0,
3706            text_rows: 0,
3707            glyph_w: 0,
3708            glyph_h: 0,
3709            double_buffer_mode: false,
3710            double_buffer_enabled: false,
3711            ui_scale: UiScale::Normal,
3712        };
3713    }
3714    VGA_WRITER.lock().framebuffer_info()
3715}
3716
3717/// Returns lightweight render metrics for profiling.
3718pub fn render_stats() -> RenderStats {
3719    RenderStats {
3720        presented_frames: PRESENTED_FRAMES.load(Ordering::Relaxed),
3721        estimated_fps: FPS_ESTIMATE.load(Ordering::Relaxed),
3722        present_region_count: VGA_PRESENT_REGION_COUNT.load(Ordering::Relaxed),
3723        present_pixel_count: VGA_PRESENT_PIXEL_COUNT.load(Ordering::Relaxed),
3724    }
3725}
3726
3727/// Sets text color.
3728pub fn set_text_color(fg: RgbColor, bg: RgbColor) {
3729    if !is_available() {
3730        return;
3731    }
3732    VGA_WRITER.lock().set_rgb_color(fg, bg);
3733}
3734
3735/// Sets clip rect.
3736pub fn set_clip_rect(x: usize, y: usize, width: usize, height: usize) {
3737    if !is_available() {
3738        return;
3739    }
3740    VGA_WRITER.lock().set_clip_rect(x, y, width, height);
3741}
3742
3743/// Performs the reset clip rect operation.
3744pub fn reset_clip_rect() {
3745    if !is_available() {
3746        return;
3747    }
3748    VGA_WRITER.lock().reset_clip_rect();
3749}
3750
3751/// Performs the begin frame operation.
3752pub fn begin_frame() -> bool {
3753    if !is_available() {
3754        return false;
3755    }
3756    if !double_buffer_mode() {
3757        return false;
3758    }
3759    VGA_WRITER.lock().enable_double_buffer()
3760}
3761
3762/// Performs the end frame operation.
3763pub fn end_frame() {
3764    if !is_available() {
3765        return;
3766    }
3767    let mut writer = VGA_WRITER.lock();
3768    writer.present();
3769    writer.disable_double_buffer(false);
3770}
3771
3772/// Performs the present operation.
3773pub fn present() {
3774    if !is_available() {
3775        return;
3776    }
3777    VGA_WRITER.lock().present();
3778}
3779
3780/// Performs the draw pixel operation.
3781pub fn draw_pixel(x: usize, y: usize, color: RgbColor) {
3782    if !is_available() {
3783        return;
3784    }
3785    VGA_WRITER.lock().draw_pixel(x, y, color);
3786}
3787
3788/// Performs the draw pixel alpha operation.
3789pub fn draw_pixel_alpha(x: usize, y: usize, color: RgbColor, alpha: u8) {
3790    if !is_available() {
3791        return;
3792    }
3793    VGA_WRITER.lock().draw_pixel_alpha(x, y, color, alpha);
3794}
3795
3796/// Performs the draw line operation.
3797pub fn draw_line(x0: isize, y0: isize, x1: isize, y1: isize, color: RgbColor) {
3798    if !is_available() {
3799        return;
3800    }
3801    VGA_WRITER.lock().draw_line(x0, y0, x1, y1, color);
3802}
3803
3804/// Performs the draw rect operation.
3805pub fn draw_rect(x: usize, y: usize, width: usize, height: usize, color: RgbColor) {
3806    if !is_available() {
3807        return;
3808    }
3809    VGA_WRITER.lock().draw_rect(x, y, width, height, color);
3810}
3811
3812/// Performs the fill rect operation.
3813pub fn fill_rect(x: usize, y: usize, width: usize, height: usize, color: RgbColor) {
3814    if !is_available() {
3815        return;
3816    }
3817    VGA_WRITER.lock().fill_rect(x, y, width, height, color);
3818}
3819
3820/// Performs the fill rect alpha operation.
3821pub fn fill_rect_alpha(
3822    x: usize,
3823    y: usize,
3824    width: usize,
3825    height: usize,
3826    color: RgbColor,
3827    alpha: u8,
3828) {
3829    if !is_available() {
3830        return;
3831    }
3832    VGA_WRITER
3833        .lock()
3834        .fill_rect_alpha(x, y, width, height, color, alpha);
3835}
3836
3837/// Performs the blit rgb operation.
3838pub fn blit_rgb(
3839    dst_x: usize,
3840    dst_y: usize,
3841    src_width: usize,
3842    src_height: usize,
3843    pixels: &[RgbColor],
3844) -> bool {
3845    if !is_available() {
3846        return false;
3847    }
3848    VGA_WRITER
3849        .lock()
3850        .blit_rgb(dst_x, dst_y, src_width, src_height, pixels)
3851}
3852
3853/// Performs the blit rgb24 operation.
3854pub fn blit_rgb24(
3855    dst_x: usize,
3856    dst_y: usize,
3857    src_width: usize,
3858    src_height: usize,
3859    bytes: &[u8],
3860) -> bool {
3861    if !is_available() {
3862        return false;
3863    }
3864    VGA_WRITER
3865        .lock()
3866        .blit_rgb24(dst_x, dst_y, src_width, src_height, bytes)
3867}
3868
3869/// Performs the blit rgba operation.
3870pub fn blit_rgba(
3871    dst_x: usize,
3872    dst_y: usize,
3873    src_width: usize,
3874    src_height: usize,
3875    bytes: &[u8],
3876    global_alpha: u8,
3877) -> bool {
3878    if !is_available() {
3879        return false;
3880    }
3881    VGA_WRITER
3882        .lock()
3883        .blit_rgba(dst_x, dst_y, src_width, src_height, bytes, global_alpha)
3884}
3885
3886/// Performs the blit sprite rgba operation.
3887pub fn blit_sprite_rgba(
3888    dst_x: usize,
3889    dst_y: usize,
3890    sprite: SpriteRgba<'_>,
3891    global_alpha: u8,
3892) -> bool {
3893    if !is_available() {
3894        return false;
3895    }
3896    VGA_WRITER
3897        .lock()
3898        .blit_sprite_rgba(dst_x, dst_y, sprite, global_alpha)
3899}
3900
3901/// Performs the draw text at operation.
3902pub fn draw_text_at(pixel_x: usize, pixel_y: usize, text: &str, fg: RgbColor, bg: RgbColor) {
3903    if !is_available() {
3904        return;
3905    }
3906    VGA_WRITER
3907        .lock()
3908        .draw_text_at(pixel_x, pixel_y, text, fg, bg);
3909}
3910
3911/// Performs the draw text operation.
3912pub fn draw_text(pixel_x: usize, pixel_y: usize, text: &str, opts: TextOptions) -> TextMetrics {
3913    if !is_available() {
3914        return TextMetrics {
3915            width: 0,
3916            height: 0,
3917            lines: 0,
3918        };
3919    }
3920    VGA_WRITER.lock().draw_text(pixel_x, pixel_y, text, opts)
3921}
3922
3923/// Performs the measure text operation.
3924pub fn measure_text(text: &str, max_width: Option<usize>, wrap: bool) -> TextMetrics {
3925    if !is_available() {
3926        return TextMetrics {
3927            width: 0,
3928            height: 0,
3929            lines: 0,
3930        };
3931    }
3932    VGA_WRITER.lock().measure_text(text, max_width, wrap)
3933}
3934
3935/// Performs the ui clear operation.
3936pub fn ui_clear(theme: UiTheme) {
3937    let _ = with_writer(|w| w.clear_with(theme.background));
3938}
3939
3940/// Performs the ui draw panel operation.
3941pub fn ui_draw_panel(
3942    x: usize,
3943    y: usize,
3944    width: usize,
3945    height: usize,
3946    title: &str,
3947    body: &str,
3948    theme: UiTheme,
3949) {
3950    let _ = with_writer(|w| {
3951        if width < 8 || height < 8 {
3952            return;
3953        }
3954        let (gw, gh) = w.glyph_size();
3955        w.fill_rect(x, y, width, height, theme.panel_bg);
3956        w.draw_rect(x, y, width, height, theme.panel_border);
3957
3958        let title_h = gh + 6;
3959        w.fill_rect(
3960            x.saturating_add(1),
3961            y.saturating_add(1),
3962            width.saturating_sub(2),
3963            title_h,
3964            theme.accent,
3965        );
3966        let title_opts = TextOptions {
3967            fg: theme.text,
3968            bg: theme.accent,
3969            align: TextAlign::Left,
3970            wrap: false,
3971            max_width: Some(width.saturating_sub(10)),
3972        };
3973        w.draw_text(x.saturating_add(6), y.saturating_add(3), title, title_opts);
3974
3975        let body_opts = TextOptions {
3976            fg: theme.text,
3977            bg: theme.panel_bg,
3978            align: TextAlign::Left,
3979            wrap: true,
3980            max_width: Some(width.saturating_sub(10)),
3981        };
3982        w.draw_text(
3983            x.saturating_add(6),
3984            y.saturating_add(title_h + 4),
3985            body,
3986            body_opts,
3987        );
3988
3989        // Visual separator.
3990        w.fill_rect(
3991            x.saturating_add(1),
3992            y.saturating_add(title_h + 1),
3993            width.saturating_sub(2),
3994            1,
3995            theme.panel_border,
3996        );
3997        // Keep an implicit reference to glyph width to avoid dead code warning for gw in tiny fonts.
3998        let _ = gw;
3999    });
4000}
4001
4002/// Performs the ui draw panel widget operation.
4003pub fn ui_draw_panel_widget(panel: &UiPanel<'_>) {
4004    ui_draw_panel(
4005        panel.rect.x,
4006        panel.rect.y,
4007        panel.rect.w,
4008        panel.rect.h,
4009        panel.title,
4010        panel.body,
4011        panel.theme,
4012    );
4013}
4014
4015/// Performs the ui draw label operation.
4016pub fn ui_draw_label(label: &UiLabel<'_>) {
4017    let _ = with_writer(|w| {
4018        w.draw_text(
4019            label.rect.x,
4020            label.rect.y,
4021            label.text,
4022            TextOptions {
4023                fg: label.fg,
4024                bg: label.bg,
4025                align: label.align,
4026                wrap: false,
4027                max_width: Some(label.rect.w),
4028            },
4029        );
4030    });
4031}
4032
4033/// Performs the ui draw progress bar operation.
4034pub fn ui_draw_progress_bar(bar: UiProgressBar) {
4035    let _ = with_writer(|w| {
4036        if bar.rect.w < 3 || bar.rect.h < 3 {
4037            return;
4038        }
4039        let value = core::cmp::min(bar.value, 100) as usize;
4040        w.fill_rect(bar.rect.x, bar.rect.y, bar.rect.w, bar.rect.h, bar.bg);
4041        w.draw_rect(bar.rect.x, bar.rect.y, bar.rect.w, bar.rect.h, bar.border);
4042        let inner_w = bar.rect.w.saturating_sub(2);
4043        let fill_w = inner_w.saturating_mul(value) / 100;
4044        if fill_w > 0 {
4045            w.fill_rect(
4046                bar.rect.x + 1,
4047                bar.rect.y + 1,
4048                fill_w,
4049                bar.rect.h.saturating_sub(2),
4050                bar.fg,
4051            );
4052        }
4053    });
4054}
4055
4056/// Performs the ui draw table operation.
4057pub fn ui_draw_table(table: &UiTable) {
4058    let _ = with_writer(|w| {
4059        if table.rect.w < 8 || table.rect.h < 8 {
4060            return;
4061        }
4062        let (_gw, gh) = w.glyph_size();
4063        if gh == 0 {
4064            return;
4065        }
4066
4067        w.fill_rect(
4068            table.rect.x,
4069            table.rect.y,
4070            table.rect.w,
4071            table.rect.h,
4072            table.theme.panel_bg,
4073        );
4074        w.draw_rect(
4075            table.rect.x,
4076            table.rect.y,
4077            table.rect.w,
4078            table.rect.h,
4079            table.theme.panel_border,
4080        );
4081
4082        let cols = core::cmp::max(1, table.headers.len());
4083        let col_w = table.rect.w / cols;
4084        let header_h = gh + 2;
4085        w.fill_rect(
4086            table.rect.x + 1,
4087            table.rect.y + 1,
4088            table.rect.w.saturating_sub(2),
4089            header_h,
4090            table.theme.accent,
4091        );
4092
4093        for (i, h) in table.headers.iter().enumerate() {
4094            let x = table.rect.x + i * col_w + 2;
4095            w.draw_text(
4096                x,
4097                table.rect.y + 1,
4098                h,
4099                TextOptions {
4100                    fg: table.theme.text,
4101                    bg: table.theme.accent,
4102                    align: TextAlign::Left,
4103                    wrap: false,
4104                    max_width: Some(col_w.saturating_sub(4)),
4105                },
4106            );
4107        }
4108
4109        let mut y = table.rect.y + header_h + 2;
4110        for row in &table.rows {
4111            if y + gh > table.rect.y + table.rect.h {
4112                break;
4113            }
4114            for c in 0..cols {
4115                if c >= row.len() {
4116                    continue;
4117                }
4118                let x = table.rect.x + c * col_w + 2;
4119                w.draw_text(
4120                    x,
4121                    y,
4122                    &row[c],
4123                    TextOptions {
4124                        fg: table.theme.text,
4125                        bg: table.theme.panel_bg,
4126                        align: TextAlign::Left,
4127                        wrap: false,
4128                        max_width: Some(col_w.saturating_sub(4)),
4129                    },
4130                );
4131            }
4132            y += gh;
4133        }
4134    });
4135}
4136
4137/// Performs the ui draw status bar operation.
4138pub fn ui_draw_status_bar(left: &str, right: &str, theme: UiTheme) {
4139    let _ = with_writer(|w| {
4140        draw_status_bar_inner(w, left, right, theme);
4141    });
4142}
4143
4144/// Sets status hostname.
4145pub fn set_status_hostname(hostname: &str) {
4146    let mut guard = STATUS_LINE_INFO.lock();
4147    if guard.is_none() {
4148        *guard = Some(StatusLineInfo {
4149            hostname: String::new(),
4150            ip: String::from("n/a"),
4151        });
4152    }
4153    if let Some(info) = guard.as_mut() {
4154        info.hostname.clear();
4155        info.hostname.push_str(hostname);
4156    }
4157}
4158
4159/// Sets status ip.
4160pub fn set_status_ip(ip: &str) {
4161    let mut guard = STATUS_LINE_INFO.lock();
4162    if guard.is_none() {
4163        *guard = Some(StatusLineInfo {
4164            hostname: String::from("strat9"),
4165            ip: String::new(),
4166        });
4167    }
4168    if let Some(info) = guard.as_mut() {
4169        info.ip.clear();
4170        info.ip.push_str(ip);
4171    }
4172}
4173
4174/// Performs the draw system status line operation.
4175pub fn draw_system_status_line(theme: UiTheme) {
4176    let info = status_line_info();
4177    let version = env!("CARGO_PKG_VERSION");
4178    let tick = crate::process::scheduler::ticks();
4179    let uptime = format_uptime_from_ticks(tick);
4180    let mem = format_mem_usage();
4181    let fps = current_fps(tick);
4182    let left = format!(" {} ", info.hostname);
4183    let right = format!(
4184        "ip:{}  ver:{}  uptime:{}  ticks:{}  FPS:{}  load:n/a  memfree:{} ",
4185        info.ip, version, uptime, tick, fps, mem
4186    );
4187    ui_draw_status_bar(&left, &right, theme);
4188}
4189
4190/// Performs the draw boot status line operation.
4191fn draw_boot_status_line(theme: UiTheme) {
4192    let _ = with_writer(|w| {
4193        draw_status_bar_inner(
4194            w,
4195            " strat9 ",
4196            "ip:n/a  ver:boot  up:00:00:00  ticks:0  load:n/a  mem:n/a ",
4197            theme,
4198        );
4199    });
4200}
4201
4202/// Performs the refresh status ip from net scheme operation.
4203fn refresh_status_ip_from_net_scheme() {
4204    let tick = crate::process::scheduler::ticks();
4205    let last = STATUS_LAST_IP_REFRESH_TICK.load(Ordering::Relaxed);
4206    if tick.saturating_sub(last) < STATUS_IP_REFRESH_PERIOD_TICKS {
4207        return;
4208    }
4209    if STATUS_LAST_IP_REFRESH_TICK
4210        .compare_exchange(last, tick, Ordering::Relaxed, Ordering::Relaxed)
4211        .is_err()
4212    {
4213        return;
4214    }
4215
4216    let paths = ["/net/address", "/net/ip"];
4217    for path in paths {
4218        let fd = match crate::vfs::open(path, crate::vfs::OpenFlags::READ) {
4219            Ok(fd) => fd,
4220            Err(_) => continue,
4221        };
4222        let mut buf = [0u8; 64];
4223        let read_res = crate::vfs::read(fd, &mut buf);
4224        let _ = crate::vfs::close(fd);
4225        let n = match read_res {
4226            Ok(n) => n,
4227            Err(_) => continue,
4228        };
4229        if n == 0 {
4230            continue;
4231        }
4232        let Ok(text) = core::str::from_utf8(&buf[..n]) else {
4233            continue;
4234        };
4235        let mut ip = text.trim();
4236        if let Some(slash) = ip.find('/') {
4237            ip = &ip[..slash];
4238        }
4239        if ip.is_empty() || ip == "0.0.0.0" || ip == "169.254.0.0" {
4240            continue;
4241        }
4242        set_status_ip(ip);
4243        return;
4244    }
4245
4246    set_status_ip("n/a");
4247}
4248
4249/// Stack-only string buffer to avoid heap allocation in the status-line path.
4250struct StackStr<const N: usize> {
4251    buf: [u8; N],
4252    len: usize,
4253}
4254
4255impl<const N: usize> StackStr<N> {
4256    /// Creates a new instance.
4257    const fn new() -> Self {
4258        Self {
4259            buf: [0; N],
4260            len: 0,
4261        }
4262    }
4263    /// Returns this as str.
4264    fn as_str(&self) -> &str {
4265        unsafe { core::str::from_utf8_unchecked(&self.buf[..self.len]) }
4266    }
4267}
4268
4269impl<const N: usize> core::fmt::Write for StackStr<N> {
4270    /// Writes str.
4271    fn write_str(&mut self, s: &str) -> core::fmt::Result {
4272        let bytes = s.as_bytes();
4273        let avail = N - self.len;
4274        let n = bytes.len().min(avail);
4275        self.buf[self.len..self.len + n].copy_from_slice(&bytes[..n]);
4276        self.len += n;
4277        Ok(())
4278    }
4279}
4280
4281/// Performs the maybe refresh system status line operation.
4282pub fn maybe_refresh_system_status_line(theme: UiTheme) {
4283    if !is_available() {
4284        return;
4285    }
4286
4287    let tick = crate::process::scheduler::ticks();
4288    let last = STATUS_LAST_REFRESH_TICK.load(Ordering::Relaxed);
4289    if tick.saturating_sub(last) < STATUS_REFRESH_PERIOD_TICKS {
4290        return;
4291    }
4292    if STATUS_LAST_REFRESH_TICK
4293        .compare_exchange(last, tick, Ordering::Relaxed, Ordering::Relaxed)
4294        .is_err()
4295    {
4296        return;
4297    }
4298    refresh_status_ip_from_net_scheme();
4299
4300    let (hostname, ip) = if let Some(guard) = STATUS_LINE_INFO.try_lock() {
4301        if let Some(info) = guard.as_ref() {
4302            let mut h = StackStr::<64>::new();
4303            let mut i = StackStr::<48>::new();
4304            let _ = core::fmt::Write::write_str(&mut h, &info.hostname);
4305            let _ = core::fmt::Write::write_str(&mut i, &info.ip);
4306            (h, i)
4307        } else {
4308            let mut h = StackStr::<64>::new();
4309            let mut i = StackStr::<48>::new();
4310            let _ = core::fmt::Write::write_str(&mut h, "strat9");
4311            let _ = core::fmt::Write::write_str(&mut i, "n/a");
4312            (h, i)
4313        }
4314    } else {
4315        return;
4316    };
4317
4318    let version = env!("CARGO_PKG_VERSION");
4319
4320    let total_secs = tick / 100;
4321    let h = total_secs / 3600;
4322    let m = (total_secs % 3600) / 60;
4323    let s = total_secs % 60;
4324
4325    let mem_str = {
4326        use core::fmt::Write;
4327        let lock = crate::memory::buddy::get_allocator();
4328        if let Some(guard) = lock.try_lock() {
4329            if let Some(alloc) = guard.as_ref() {
4330                let (tp, ap) = alloc.page_totals();
4331                let total = tp.saturating_mul(4096);
4332                let used = ap.saturating_mul(4096);
4333                let free = total.saturating_sub(used);
4334                let mut buf = StackStr::<32>::new();
4335                let _ = write!(
4336                    buf,
4337                    "{}/{}",
4338                    format_size_stack(free),
4339                    format_size_stack(total)
4340                );
4341                buf
4342            } else {
4343                let mut buf = StackStr::<32>::new();
4344                let _ = core::fmt::Write::write_str(&mut buf, "n/a");
4345                buf
4346            }
4347        } else {
4348            let mut buf = StackStr::<32>::new();
4349            let _ = core::fmt::Write::write_str(&mut buf, "n/a");
4350            buf
4351        }
4352    };
4353
4354    let fps = current_fps(tick);
4355
4356    use core::fmt::Write;
4357    let mut left = StackStr::<80>::new();
4358    let _ = write!(left, " {} ", hostname.as_str());
4359
4360    let mut right = StackStr::<256>::new();
4361    let _ = write!(
4362        right,
4363        "ip:{}  ver:{}  up:{:02}:{:02}:{:02}  ticks:{}  fps:{}  load:n/a  mem:{} ",
4364        ip.as_str(),
4365        version,
4366        h,
4367        m,
4368        s,
4369        tick,
4370        fps,
4371        mem_str.as_str()
4372    );
4373
4374    if let Some(mut writer) = VGA_WRITER.try_lock() {
4375        draw_status_bar_inner(&mut writer, left.as_str(), right.as_str(), theme);
4376    }
4377}
4378
4379/// Performs the format size stack operation.
4380fn format_size_stack(bytes: usize) -> StackStr<16> {
4381    use core::fmt::Write;
4382    const KB: usize = 1024;
4383    const MB: usize = 1024 * KB;
4384    const GB: usize = 1024 * MB;
4385    let mut buf = StackStr::<16>::new();
4386    if bytes >= GB {
4387        let _ = write!(buf, "{}G", bytes / GB);
4388    } else if bytes >= MB {
4389        let _ = write!(buf, "{}M", bytes / MB);
4390    } else if bytes >= KB {
4391        let _ = write!(buf, "{}K", bytes / KB);
4392    } else {
4393        let _ = write!(buf, "{}B", bytes);
4394    }
4395    buf
4396}
4397
4398impl<const N: usize> core::fmt::Display for StackStr<N> {
4399    /// Performs the fmt operation.
4400    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
4401        f.write_str(self.as_str())
4402    }
4403}
4404
4405/// Performs the status line task main operation.
4406pub extern "C" fn status_line_task_main() -> ! {
4407    let mut last_tick = 0u64;
4408    let mut diag_counter = 0u64;
4409    loop {
4410        let tick = crate::process::scheduler::ticks();
4411        if tick != last_tick {
4412            last_tick = tick;
4413            maybe_refresh_system_status_line(UiTheme::OCEAN_STATUS);
4414        }
4415        diag_counter += 1;
4416        if diag_counter % 5000 == 0 {
4417            crate::serial_println!(
4418                "[status-line] heartbeat tick={} vga={}",
4419                tick,
4420                is_available()
4421            );
4422        }
4423        crate::process::yield_task();
4424    }
4425}
4426
4427/// Performs the draw strata stack operation.
4428pub fn draw_strata_stack(origin_x: usize, origin_y: usize, layer_w: usize, layer_h: usize) {
4429    if !is_available() {
4430        return;
4431    }
4432    VGA_WRITER
4433        .lock()
4434        .draw_strata_stack(origin_x, origin_y, layer_w, layer_h);
4435}
4436//  Scrollback / scrollbar public API ==================================================================================================================================
4437
4438/// Scroll the console view up (backward in history) by `lines` lines.
4439pub fn scroll_view_up(lines: usize) {
4440    if !is_available() {
4441        return;
4442    }
4443    VGA_WRITER.lock().scroll_view_up(lines);
4444}
4445
4446/// Scroll the console view down (forward, toward live output) by `lines` lines.
4447pub fn scroll_view_down(lines: usize) {
4448    if !is_available() {
4449        return;
4450    }
4451    VGA_WRITER.lock().scroll_view_down(lines);
4452}
4453
4454/// Return immediately to the live (bottom) view.
4455pub fn scroll_to_live() {
4456    if !is_available() {
4457        return;
4458    }
4459    if let Some(mut w) = VGA_WRITER.try_lock() {
4460        w.scroll_to_live();
4461    }
4462}
4463
4464/// Handle a click at framebuffer pixel `(px_x, px_y)`.
4465/// If the click lands on the scrollbar, jump the view accordingly.
4466pub fn scrollbar_click(px_x: usize, px_y: usize) {
4467    if !is_available() {
4468        return;
4469    }
4470    if let Some(mut w) = VGA_WRITER.try_lock() {
4471        w.scrollbar_click(px_x, px_y);
4472    }
4473}
4474
4475/// Drag the scrollbar to a given Y pixel coordinate.
4476pub fn scrollbar_drag_to(px_y: usize) {
4477    if !is_available() {
4478        return;
4479    }
4480    if let Some(mut w) = VGA_WRITER.try_lock() {
4481        w.scrollbar_drag_to(px_y);
4482    }
4483}
4484
4485/// Returns `true` if `(px_x, px_y)` falls within the scrollbar strip.
4486pub fn scrollbar_hit_test(px_x: usize, px_y: usize) -> bool {
4487    if !is_available() {
4488        return false;
4489    }
4490    if let Some(w) = VGA_WRITER.try_lock() {
4491        w.scrollbar_hit_test(px_x, px_y)
4492    } else {
4493        false
4494    }
4495}
4496
4497/// Updates mouse cursor.
4498pub fn update_mouse_cursor(x: i32, y: i32) {
4499    if !is_available() {
4500        return;
4501    }
4502    if let Some(mut w) = VGA_WRITER.try_lock() {
4503        w.update_mouse_cursor(x, y);
4504    }
4505}
4506
4507/// Performs the hide mouse cursor operation.
4508pub fn hide_mouse_cursor() {
4509    if !is_available() {
4510        return;
4511    }
4512    if let Some(mut w) = VGA_WRITER.try_lock() {
4513        w.hide_mouse_cursor();
4514    }
4515}
4516
4517/// Starts selection.
4518pub fn start_selection(px: usize, py: usize) {
4519    if !is_available() {
4520        return;
4521    }
4522    if let Some(mut w) = VGA_WRITER.try_lock() {
4523        w.start_selection(px, py);
4524    }
4525}
4526
4527/// Updates selection.
4528pub fn update_selection(px: usize, py: usize) {
4529    if !is_available() {
4530        return;
4531    }
4532    if let Some(mut w) = VGA_WRITER.try_lock() {
4533        w.update_selection(px, py);
4534    }
4535}
4536
4537/// Performs the end selection operation.
4538pub fn end_selection() {
4539    if !is_available() {
4540        return;
4541    }
4542    if let Some(mut w) = VGA_WRITER.try_lock() {
4543        w.end_selection();
4544    }
4545}
4546
4547/// Performs the clear selection operation.
4548pub fn clear_selection() {
4549    if !is_available() {
4550        return;
4551    }
4552    if let Some(mut w) = VGA_WRITER.try_lock() {
4553        w.clear_selection();
4554    }
4555}
4556
4557/// Returns clipboard text.
4558pub fn get_clipboard_text(buf: &mut [u8]) -> usize {
4559    if let Some(clip) = CLIPBOARD.try_lock() {
4560        let n = clip.1.min(buf.len());
4561        buf[..n].copy_from_slice(&clip.0[..n]);
4562        n
4563    } else {
4564        0
4565    }
4566}