Skip to main content

strat9_kernel/arch/x86_64/
vgabuf.rs

1//! Lock-free circular buffer for VGA log lines.
2//!
3//! The kernel's `log::info!` / `log::warn!` / etc. macros write to this buffer
4//! instead of directly locking `VGA_WRITER`.  A background display task (or the
5//! panic handler) calls `vgabuf_flush_to_framebuffer()` to render the buffered
6//! lines onto the actual framebuffer.
7//!
8//! This decouples the hot logging path from the (comparatively) slow framebuffer
9//! drawing, preventing deadlocks and keeping log output fast.
10
11use core::{
12    cell::UnsafeCell,
13    sync::atomic::{AtomicU16, AtomicUsize, Ordering},
14};
15
16// ---------------------------------------------------------------------------
17// Constants
18// ---------------------------------------------------------------------------
19
20/// Maximum number of lines stored in the circular buffer.
21/// 512 gives ample headroom: at 100Hz flush × 30 lines/batch, the writer
22/// would need to produce >512 log messages between two reader ticks to
23/// overwrite an unread slot.
24pub const VGABUF_CAPACITY: usize = 512;
25
26/// Maximum byte length of a single log line (including trailing newline).
27pub const VGABUF_LINE_LEN: usize = 256;
28
29// ---------------------------------------------------------------------------
30// Global buffer state
31// ---------------------------------------------------------------------------
32
33/// Raw storage: each slot is a fixed-size byte buffer.
34/// Wrapped in a newtype that is `Sync` because we only access slots via
35/// atomically-coordinated indices (single writer per slot).
36struct VgaSlot(UnsafeCell<[u8; VGABUF_LINE_LEN]>);
37unsafe impl Sync for VgaSlot {}
38
39static STORAGE: [VgaSlot; VGABUF_CAPACITY] =
40    [const { VgaSlot(UnsafeCell::new([0u8; VGABUF_LINE_LEN])) }; VGABUF_CAPACITY];
41
42/// Length (in bytes) of each stored line.  0 means the slot is unused.
43static LENS: [AtomicU16; VGABUF_CAPACITY] = [const { AtomicU16::new(0) }; VGABUF_CAPACITY];
44
45/// Next slot to write into (monotonic, wraps via `% CAPACITY`).
46static WRITE_IDX: AtomicUsize = AtomicUsize::new(0);
47
48/// Slot that the display task has rendered up to (exclusive).
49static DISPLAY_IDX: AtomicUsize = AtomicUsize::new(0);
50
51/// Total lines written since boot (for ordering / watermark).
52static TOTAL_WRITTEN: AtomicUsize = AtomicUsize::new(0);
53
54// ---------------------------------------------------------------------------
55// Public API : writer side
56// ---------------------------------------------------------------------------
57
58/// Write a line into the circular buffer.
59///
60/// `line` should be **pre-formatted** (including colour codes if desired) and
61/// should not exceed `VGABUF_LINE_LEN - 1` bytes.  Trailing bytes beyond the
62/// capacity are silently truncated.
63///
64/// This is **lock-free** (single atomic increment) and safe to call from
65/// interrupt handlers or any CPU core.
66pub fn vgabuf_write(line: &[u8]) {
67    let len = line.len().min(VGABUF_LINE_LEN - 1);
68    if len == 0 {
69        return;
70    }
71
72    // Claim the next slot.
73    let slot = WRITE_IDX.fetch_add(1, Ordering::Relaxed) % VGABUF_CAPACITY;
74
75    // If the slot is still occupied (reader hasn't consumed it yet), skip
76    // this message rather than overwriting unread data.
77    if LENS[slot].load(Ordering::Acquire) != 0 {
78        return;
79    }
80
81    // Copy data into the slot.
82    // SAFETY: each slot is owned by the writer that claimed it (one writer per
83    // fetch_add call).  No other thread will write to this slot concurrently.
84    let cell = &STORAGE[slot].0;
85    let ptr = cell.get() as *mut u8;
86    unsafe {
87        // Write bytes one-by-one (volatile not needed: the reader will see the
88        // finalised length via the LENS store-with-release below).
89        for i in 0..len {
90            core::ptr::write(ptr.add(i), line[i]);
91        }
92        // Zero the rest to avoid leaking old data.
93        if len < VGABUF_LINE_LEN {
94            core::ptr::write_bytes(ptr.add(len), 0, VGABUF_LINE_LEN - len);
95        }
96    }
97    // Publish the length : reader sees this after a successful acquire load.
98    LENS[slot].store(len as u16, Ordering::Release);
99    TOTAL_WRITTEN.fetch_add(1, Ordering::Relaxed);
100}
101
102/// Return the total number of lines written since boot.
103pub fn vgabuf_total_written() -> usize {
104    TOTAL_WRITTEN.load(Ordering::Relaxed)
105}
106
107// ---------------------------------------------------------------------------
108// Public API : reader / display side
109// ---------------------------------------------------------------------------
110
111/// Maximum number of lines we render in one flush call (to keep frame time
112/// bounded).
113const FLUSH_BATCH: usize = 30;
114
115/// Read buffered lines and render them onto the framebuffer via
116/// `crate::arch::x86_64::vga::VGA_WRITER`.
117///
118/// Call this periodically (e.g. from the status-line task or after a batch of
119/// `log::info!` calls).  It consumes at most `FLUSH_BATCH` lines per call to
120/// keep individual frame times predictable.
121///
122/// This function **locks `VGA_WRITER`** and is **not** safe to call from
123/// interrupt handlers.
124pub fn vgabuf_flush_to_framebuffer() {
125    use core::fmt::Write;
126
127    if !crate::arch::x86_64::vga::is_available() {
128        // No framebuffer : drain and discard.
129        vgabuf_drain_discard();
130        return;
131    }
132
133    let Some(mut writer) = crate::arch::x86_64::vga::VGA_WRITER.try_lock() else {
134        return; // Already locked : try again next cycle.
135    };
136
137    let mut flushed = 0usize;
138    loop {
139        let slot = DISPLAY_IDX.load(Ordering::Relaxed) % VGABUF_CAPACITY;
140        let len = LENS[slot].load(Ordering::Acquire);
141        if len == 0 {
142            break; // No more new lines.
143        }
144
145        // Read the line content.
146        // SAFETY: LENS[slot] is non-zero, so the slot was published by a writer.
147        let cell = &STORAGE[slot].0;
148        let ptr = cell.get() as *const u8;
149        let line = unsafe { core::slice::from_raw_parts(ptr, len as usize) };
150
151        // Write to the framebuffer terminal, ensuring each line ends with \n.
152        if let Ok(s) = core::str::from_utf8(line) {
153            let _ = writer.write_str(s);
154            let _ = writer.write_str("\n");
155        } else {
156            let _ = writer.write_str("<non-utf8>\n");
157        }
158
159        // Mark slot as consumed.
160        LENS[slot].store(0, Ordering::Release);
161        DISPLAY_IDX.fetch_add(1, Ordering::Relaxed);
162        flushed += 1;
163
164        if flushed >= FLUSH_BATCH {
165            break; // Yield to the scheduler; resume on next cycle.
166        }
167    }
168
169    // Only present if we actually wrote something to the framebuffer.
170    if flushed > 0 {
171        writer.present();
172    }
173    // Drop writer to release the lock.
174}
175
176/// Drain all pending lines without rendering (e.g. framebuffer not available).
177fn vgabuf_drain_discard() {
178    loop {
179        let slot = DISPLAY_IDX.load(Ordering::Relaxed) % VGABUF_CAPACITY;
180        let len = LENS[slot].load(Ordering::Acquire);
181        if len == 0 {
182            break;
183        }
184        LENS[slot].store(0, Ordering::Release);
185        DISPLAY_IDX.fetch_add(1, Ordering::Relaxed);
186    }
187}
188
189/// Flush all remaining lines (used by the panic handler before halting).
190pub fn vgabuf_flush_all() {
191    use core::fmt::Write;
192
193    if !crate::arch::x86_64::vga::is_available() {
194        vgabuf_drain_discard();
195        return;
196    }
197
198    let Some(mut writer) = crate::arch::x86_64::vga::VGA_WRITER.try_lock() else {
199        // Cannot lock : fall back to direct framebuffer drawing.
200        vgabuf_flush_all_direct();
201        return;
202    };
203
204    loop {
205        let slot = DISPLAY_IDX.load(Ordering::Relaxed) % VGABUF_CAPACITY;
206        let len = LENS[slot].load(Ordering::Acquire);
207        if len == 0 {
208            break;
209        }
210        let cell = &STORAGE[slot].0;
211        let ptr = cell.get() as *const u8;
212        let line = unsafe { core::slice::from_raw_parts(ptr, len as usize) };
213        if let Ok(s) = core::str::from_utf8(line) {
214            let _ = writer.write_str(s);
215        }
216        LENS[slot].store(0, Ordering::Release);
217        DISPLAY_IDX.fetch_add(1, Ordering::Relaxed);
218    }
219    writer.present();
220}
221
222/// Fallback when VGA_WRITER is locked: use the direct panic drawer.
223/// Uses a fixed-size stack buffer to avoid heap allocation in panic context.
224fn vgabuf_flush_all_direct() {
225    // Fixed buffer: max 64 pending lines. If more, excess lines are discarded.
226    const MAX_LINES: usize = 64;
227    let mut lines: [&str; MAX_LINES] = [""; MAX_LINES];
228    let mut count = 0usize;
229    loop {
230        let slot = DISPLAY_IDX.load(Ordering::Relaxed) % VGABUF_CAPACITY;
231        let len = LENS[slot].load(Ordering::Acquire);
232        if len == 0 {
233            break;
234        }
235        let cell = &STORAGE[slot].0;
236        let ptr = cell.get() as *const u8;
237        let line = unsafe { core::slice::from_raw_parts(ptr, len as usize) };
238        if let Ok(s) = core::str::from_utf8(line) {
239            if count < MAX_LINES {
240                lines[count] = s;
241                count += 1;
242            }
243        }
244        LENS[slot].store(0, Ordering::Release);
245        DISPLAY_IDX.fetch_add(1, Ordering::Relaxed);
246    }
247    if count > 0 {
248        crate::arch::x86_64::vga::panic_draw_direct(&lines[..count]);
249    }
250}