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}