Skip to main content

strat9_kernel/arch/x86_64/vga/
debug_overlay.rs

1/// Live debug framebuffer overlay.
2///
3/// Writes directly to the VGA framebuffer, bypassing VGA_WRITER and all
4/// buffers/locks.  Enabled by `debug_cfg::VGA_DEBUG_LIVE`.
5use super::panic_screen::{
6    panic_pack_rgb, PANIC_FB_ADDR, PANIC_FB_FORMAT, PANIC_FB_PITCH, PANIC_FONT_8X16,
7};
8
9// bypassing all buffers and locks.  Enabled by `debug_cfg::VGA_DEBUG_LIVE`.
10// =============================================================================
11
12/// Current cursor column for the live debug writer (0-based character column).
13static VGA_DEBUG_COL: core::sync::atomic::AtomicUsize = core::sync::atomic::AtomicUsize::new(0);
14/// Current cursor row for the live debug writer (0-based character row).
15static VGA_DEBUG_ROW: core::sync::atomic::AtomicUsize = core::sync::atomic::AtomicUsize::new(0);
16
17/// Maximum columns available on the current framebuffer (cached).
18static VGA_DEBUG_MAX_COL: core::sync::atomic::AtomicUsize = core::sync::atomic::AtomicUsize::new(0);
19/// Maximum rows available on the current framebuffer (cached).
20static VGA_DEBUG_MAX_ROW: core::sync::atomic::AtomicUsize = core::sync::atomic::AtomicUsize::new(0);
21
22/// Initialise or re-initialise the live debug writer's geometry.
23/// Called automatically from `vga::init()`.
24pub fn vga_debug_init(fb_width: usize, fb_height: usize) {
25    let cols = fb_width / 8; // panic_font_8x16 glyph width
26    let rows = fb_height / 16; // panic_font_8x16 glyph height
27    let status_rows = 1; // reserve 1 row for status bar
28    VGA_DEBUG_MAX_COL.store(cols, core::sync::atomic::Ordering::Relaxed);
29    VGA_DEBUG_MAX_ROW.store(
30        rows.saturating_sub(status_rows),
31        core::sync::atomic::Ordering::Relaxed,
32    );
33    VGA_DEBUG_COL.store(0, core::sync::atomic::Ordering::Relaxed);
34    VGA_DEBUG_ROW.store(0, core::sync::atomic::Ordering::Relaxed);
35}
36
37/// Scroll the live debug area up by one line.
38///
39/// Uses a single memmove to shift pixel data, then fills the exposed line
40/// with the background colour (same technique as Linux fbcon).
41fn vga_debug_scroll() {
42    let fb_addr = PANIC_FB_ADDR.load(core::sync::atomic::Ordering::Acquire);
43    if fb_addr == 0 {
44        return;
45    }
46    let pitch = PANIC_FB_PITCH.load(core::sync::atomic::Ordering::Relaxed) as usize;
47    let max_row = VGA_DEBUG_MAX_ROW.load(core::sync::atomic::Ordering::Relaxed);
48    let fmt = PANIC_FB_FORMAT.load(core::sync::atomic::Ordering::Relaxed);
49    let bpp = (fmt >> 32) as u16;
50    let bpp_bytes = (bpp / 8) as usize;
51    if bpp_bytes < 3 || pitch == 0 || max_row <= 1 {
52        return;
53    }
54
55    let fb = fb_addr as *mut u8;
56    let max_col = VGA_DEBUG_MAX_COL.load(core::sync::atomic::Ordering::Relaxed);
57    let line_bytes = 16 * pitch; // one text row in bytes
58    let total = (max_row - 1) * line_bytes; // all rows except the first
59    let bg = panic_pack_rgb(fmt, 0x12, 0x16, 0x1E);
60
61    unsafe {
62        // Shift content up by one text line.
63        core::ptr::copy(fb.add(line_bytes), fb, total);
64        // Fill the newly exposed last line.
65        let clear = fb.add((max_row - 1) * line_bytes);
66        vga_debug_fill_line(clear, pitch, max_col, bpp_bytes, bg);
67    }
68}
69
70/// Fill a single text line (16 pixel rows) with a solid colour.
71/// `max_col` is the number of character columns (each 8px wide).
72unsafe fn vga_debug_fill_line(
73    base: *mut u8,
74    pitch: usize,
75    max_col: usize,
76    bpp_bytes: usize,
77    color: u32,
78) {
79    let fill_pixels = max_col * 8; // visible pixel width only
80    if bpp_bytes == 4 {
81        for row in 0..16 {
82            let r = base.add(row * pitch) as *mut u32;
83            for p in 0..fill_pixels {
84                core::ptr::write_volatile(r.add(p), color);
85            }
86        }
87    } else {
88        let fill_bytes = fill_pixels * 3;
89        for row in 0..16 {
90            let r = base.add(row * pitch);
91            for x in (0..fill_bytes).step_by(3) {
92                core::ptr::write_volatile(r.add(x), color as u8);
93                core::ptr::write_volatile(r.add(x + 1), (color >> 8) as u8);
94                core::ptr::write_volatile(r.add(x + 2), (color >> 16) as u8);
95            }
96        }
97    }
98}
99
100/// Clear a single 8×16 character cell with the background colour.
101unsafe fn vga_debug_clear_cell(
102    fb: *mut u8,
103    pitch: usize,
104    x: usize,
105    y: usize,
106    bpp_bytes: usize,
107    bg: u32,
108) {
109    if bpp_bytes == 4 {
110        let x_off = x * 4;
111        for row in 0..16 {
112            let r = fb.add((y + row) * pitch + x_off) as *mut u32;
113            for c in 0..8 {
114                core::ptr::write_volatile(r.add(c), bg);
115            }
116        }
117    } else {
118        for row in 0..16 {
119            let r = fb.add((y + row) * pitch + x * 3);
120            for c in 0..8 {
121                let p = r.add(c * 3);
122                core::ptr::write_volatile(p, bg as u8);
123                core::ptr::write_volatile(p.add(1), (bg >> 8) as u8);
124                core::ptr::write_volatile(p.add(2), (bg >> 16) as u8);
125            }
126        }
127    }
128}
129
130/// Draw a single 8×16 character from the panic font.
131///
132/// First clears the entire cell with `bg`, then draws the glyph's foreground
133/// pixels.  This guarantees crisp, ghost-free text (like Linux fbcon).
134unsafe fn vga_debug_draw_glyph(
135    fb: *mut u8,
136    pitch: usize,
137    x: usize,
138    y: usize,
139    ch: u8,
140    fg: u32,
141    bg: u32,
142    bpp_bytes: usize,
143) {
144    // Clear cell first : no ghosting.
145    vga_debug_clear_cell(fb, pitch, x, y, bpp_bytes, bg);
146
147    let idx = if ch < 0x20 || ch > 0x7E {
148        0usize
149    } else {
150        (ch - 0x20) as usize
151    };
152    let gs = idx * 16;
153
154    if bpp_bytes == 4 {
155        for row in 0..16 {
156            let mask = PANIC_FONT_8X16[gs + row];
157            if mask == 0 {
158                continue;
159            }
160            let r = fb.add((y + row) * pitch + x * 4) as *mut u32;
161            for col in 0..8 {
162                if (mask >> (7 - col)) & 1 != 0 {
163                    core::ptr::write_volatile(r.add(col), fg);
164                }
165            }
166        }
167    } else {
168        for row in 0..16 {
169            let mask = PANIC_FONT_8X16[gs + row];
170            if mask == 0 {
171                continue;
172            }
173            for col in 0..8 {
174                if (mask >> (7 - col)) & 1 != 0 {
175                    let p = fb.add((y + row) * pitch + (x + col) * 3);
176                    core::ptr::write_volatile(p, fg as u8);
177                    core::ptr::write_volatile(p.add(1), (fg >> 8) as u8);
178                    core::ptr::write_volatile(p.add(2), (fg >> 16) as u8);
179                }
180            }
181        }
182    }
183}
184
185/// Write a string directly to the VGA framebuffer in real time.
186///
187/// Each character cell is cleared before drawing, preventing ghosting from
188/// scrolling or rapid updates.  Colours are high-contrast (light grey on
189/// dark navy : similar to Linux fbcon and FreeBSD vt(4)).
190///
191/// Newlines, carriage-returns, and tab stops are handled.  When the cursor
192/// reaches the bottom of the debug area, the content scrolls up one line.
193pub fn vga_debug_write(s: &str) {
194    let fb_addr = PANIC_FB_ADDR.load(core::sync::atomic::Ordering::Acquire);
195    if fb_addr == 0 {
196        return;
197    }
198
199    let pitch = PANIC_FB_PITCH.load(core::sync::atomic::Ordering::Relaxed) as usize;
200    let max_col = VGA_DEBUG_MAX_COL.load(core::sync::atomic::Ordering::Relaxed);
201    let max_row = VGA_DEBUG_MAX_ROW.load(core::sync::atomic::Ordering::Relaxed);
202    let fmt = PANIC_FB_FORMAT.load(core::sync::atomic::Ordering::Relaxed);
203    let bpp = (fmt >> 32) as u16;
204    let bpp_bytes = (bpp / 8) as usize;
205    if max_col == 0 || max_row == 0 || pitch == 0 || bpp_bytes < 3 {
206        return;
207    }
208
209    let fb = fb_addr as *mut u8;
210
211    // High-contrast colours (Linux vt style: light grey on navy).
212    let fg = panic_pack_rgb(fmt, 0xE2, 0xE8, 0xF0);
213    let bg = panic_pack_rgb(fmt, 0x12, 0x16, 0x1E);
214
215    let mut col = VGA_DEBUG_COL.load(core::sync::atomic::Ordering::Relaxed);
216    let mut row = VGA_DEBUG_ROW.load(core::sync::atomic::Ordering::Relaxed);
217
218    for &byte in s.as_bytes() {
219        match byte {
220            b'\n' => {
221                col = 0;
222                row += 1;
223                if row >= max_row {
224                    vga_debug_scroll();
225                    row = max_row - 1;
226                }
227            }
228            b'\r' => {
229                col = 0;
230            }
231            b'\t' => {
232                col = (col + 8) & !7;
233                if col >= max_col {
234                    col = 0;
235                    row += 1;
236                }
237                if row >= max_row {
238                    vga_debug_scroll();
239                    row = max_row - 1;
240                }
241            }
242            _ => {
243                if col >= max_col {
244                    col = 0;
245                    row += 1;
246                    if row >= max_row {
247                        vga_debug_scroll();
248                        row = max_row - 1;
249                    }
250                }
251                // SAFETY: coordinates are bounded by max_col/max_row, pitch is valid.
252                unsafe {
253                    vga_debug_draw_glyph(fb, pitch, col * 8, row * 16, byte, fg, bg, bpp_bytes);
254                }
255                col += 1;
256            }
257        }
258    }
259
260    VGA_DEBUG_COL.store(col, core::sync::atomic::Ordering::Relaxed);
261    VGA_DEBUG_ROW.store(row, core::sync::atomic::Ordering::Relaxed);
262}
263
264/// Write a newline-terminated line to the live VGA debug output.
265pub fn vga_debug_writeln(s: &str) {
266    let mut buf: [u8; 512] = [0u8; 512];
267    let max = buf.len() - 1;
268    let len = if s.len() > max {
269        // Truncate at a valid UTF-8 boundary.
270        let mut truncated = max;
271        let bytes = s.as_bytes();
272        while truncated > 0 && (bytes[truncated] & 0xC0) == 0x80 {
273            truncated -= 1;
274        }
275        truncated
276    } else {
277        s.len()
278    };
279    buf[..len].copy_from_slice(&s.as_bytes()[..len]);
280    buf[len] = b'\n';
281    if let Ok(valid) = core::str::from_utf8(&buf[..len + 1]) {
282        vga_debug_write(valid);
283    }
284}