Skip to content

Commit 5216c26

Browse files
committed
Merge remote-tracking branch 'origin/main' into feature/pty-support
2 parents cf25799 + db6d3c6 commit 5216c26

File tree

7 files changed

+1155
-56
lines changed

7 files changed

+1155
-56
lines changed
Lines changed: 358 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,358 @@
1+
//! Double-buffered framebuffer implementation.
2+
//!
3+
//! Provides a shadow buffer for off-screen rendering with page flipping support.
4+
//! All rendering happens to the shadow buffer, then `flush()` copies to hardware.
5+
6+
use alloc::vec::Vec;
7+
use core::ptr;
8+
9+
/// Represents a rectangular region that has been modified.
10+
///
11+
/// Coordinates are byte offsets on each scanline.
12+
#[derive(Debug, Clone, Copy)]
13+
pub struct DirtyRegion {
14+
/// X coordinate of top-left corner (in bytes, inclusive)
15+
pub x_start: usize,
16+
/// Y coordinate of top-left corner (in scanlines, inclusive)
17+
pub y_start: usize,
18+
/// X coordinate of bottom-right corner (in bytes, exclusive)
19+
pub x_end: usize,
20+
/// Y coordinate of bottom-right corner (in scanlines, exclusive)
21+
pub y_end: usize,
22+
}
23+
24+
impl DirtyRegion {
25+
pub fn new() -> Self {
26+
Self {
27+
x_start: usize::MAX,
28+
y_start: usize::MAX,
29+
x_end: 0,
30+
y_end: 0,
31+
}
32+
}
33+
34+
/// Check if region is empty (nothing dirty).
35+
pub fn is_empty(&self) -> bool {
36+
self.x_start >= self.x_end || self.y_start >= self.y_end
37+
}
38+
39+
/// Expand region to include a byte range on a scanline.
40+
pub fn mark_dirty(&mut self, y: usize, x_start: usize, x_end: usize) {
41+
self.x_start = self.x_start.min(x_start);
42+
self.x_end = self.x_end.max(x_end);
43+
self.y_start = self.y_start.min(y);
44+
self.y_end = self.y_end.max(y.saturating_add(1));
45+
}
46+
47+
/// Reset to empty.
48+
pub fn clear(&mut self) {
49+
*self = Self::new();
50+
}
51+
}
52+
53+
/// Double-buffered framebuffer for tear-free rendering.
54+
///
55+
/// Maintains a shadow buffer in heap memory that mirrors the hardware framebuffer.
56+
/// All writes go to the shadow buffer, and `flush()` copies to the hardware buffer.
57+
pub struct DoubleBufferedFrameBuffer {
58+
/// Pointer to hardware framebuffer memory (from bootloader)
59+
hardware_ptr: *mut u8,
60+
/// Length of hardware buffer in bytes
61+
hardware_len: usize,
62+
/// Shadow buffer for off-screen rendering (heap allocated)
63+
shadow_buffer: Vec<u8>,
64+
/// Track if shadow buffer has been modified since last flush
65+
dirty: bool,
66+
/// Track the bounding box of modified regions
67+
dirty_region: DirtyRegion,
68+
/// Bytes per scanline
69+
stride: usize,
70+
/// Number of scanlines
71+
height: usize,
72+
}
73+
74+
impl DoubleBufferedFrameBuffer {
75+
/// Create a new double-buffered framebuffer.
76+
///
77+
/// Allocates a shadow buffer on the heap that mirrors the hardware framebuffer.
78+
///
79+
/// # Arguments
80+
/// * `hardware_ptr` - Pointer to the hardware framebuffer memory
81+
/// * `hardware_len` - Length of the hardware buffer in bytes
82+
/// * `stride` - Bytes per scanline
83+
/// * `height` - Number of scanlines
84+
pub fn new(hardware_ptr: *mut u8, hardware_len: usize, stride: usize, height: usize) -> Self {
85+
let mut shadow_buffer = Vec::with_capacity(hardware_len);
86+
shadow_buffer.resize(hardware_len, 0);
87+
88+
Self {
89+
hardware_ptr,
90+
hardware_len,
91+
shadow_buffer,
92+
dirty: false,
93+
dirty_region: DirtyRegion::new(),
94+
stride,
95+
height,
96+
}
97+
}
98+
99+
/// Get mutable access to the shadow buffer for rendering.
100+
#[inline]
101+
pub fn buffer_mut(&mut self) -> &mut [u8] {
102+
&mut self.shadow_buffer
103+
}
104+
105+
/// Copy the shadow buffer to the hardware framebuffer.
106+
///
107+
/// This is the "page flip" operation that makes rendered content visible.
108+
pub fn flush(&mut self) {
109+
if !self.dirty || self.dirty_region.is_empty() {
110+
self.dirty = false;
111+
self.dirty_region.clear();
112+
return;
113+
}
114+
115+
let y_start = self.dirty_region.y_start.min(self.height);
116+
let y_end = self.dirty_region.y_end.min(self.height);
117+
let x_start = self.dirty_region.x_start.min(self.stride);
118+
let x_end = self.dirty_region.x_end.min(self.stride);
119+
let max_len = self.hardware_len.min(self.shadow_buffer.len());
120+
121+
if y_start >= y_end || x_start >= x_end || max_len == 0 {
122+
self.dirty = false;
123+
self.dirty_region.clear();
124+
return;
125+
}
126+
127+
for y in y_start..y_end {
128+
let row_offset = y * self.stride;
129+
let src_start = row_offset + x_start;
130+
let src_end = row_offset + x_end;
131+
if src_end > max_len {
132+
continue;
133+
}
134+
135+
let len = x_end - x_start;
136+
if len == 0 {
137+
continue;
138+
}
139+
140+
// SAFETY: hardware_ptr is valid for hardware_len bytes (from bootloader),
141+
// shadow_buffer is valid for its length, and we copy the minimum of both.
142+
unsafe {
143+
let src = self.shadow_buffer.as_ptr().add(src_start);
144+
let dst = self.hardware_ptr.add(src_start);
145+
ptr::copy_nonoverlapping(src, dst, len);
146+
}
147+
}
148+
self.dirty = false;
149+
self.dirty_region.clear();
150+
}
151+
152+
/// Force a full buffer flush (used for clear operations).
153+
pub fn flush_full(&mut self) {
154+
let len = self.hardware_len.min(self.shadow_buffer.len());
155+
if len > 0 {
156+
// SAFETY: hardware_ptr is valid for hardware_len bytes (from bootloader),
157+
// shadow_buffer is valid for its length, and we copy the minimum of both.
158+
unsafe {
159+
ptr::copy_nonoverlapping(self.shadow_buffer.as_ptr(), self.hardware_ptr, len);
160+
}
161+
}
162+
self.dirty = false;
163+
self.dirty_region.clear();
164+
}
165+
166+
/// Mark a rectangular region as dirty (in byte coordinates).
167+
pub fn mark_region_dirty(&mut self, y: usize, x_start: usize, x_end: usize) {
168+
self.dirty = true;
169+
self.dirty_region.mark_dirty(y, x_start, x_end);
170+
}
171+
172+
/// Flush only if the buffer has been modified since the last flush.
173+
#[inline]
174+
pub fn flush_if_dirty(&mut self) {
175+
if self.dirty {
176+
self.flush();
177+
}
178+
}
179+
180+
/// Shift hardware buffer up by the given byte count.
181+
///
182+
/// Assumes the shadow buffer has already been scrolled the same way.
183+
pub fn scroll_hardware_up(&mut self, scroll_bytes: usize) {
184+
let len = self.hardware_len.min(self.shadow_buffer.len());
185+
if scroll_bytes >= len {
186+
return;
187+
}
188+
189+
// SAFETY: hardware_ptr is valid for hardware_len bytes. ptr::copy handles overlap.
190+
unsafe {
191+
let src = self.hardware_ptr.add(scroll_bytes);
192+
ptr::copy(src, self.hardware_ptr, len - scroll_bytes);
193+
}
194+
}
195+
}
196+
197+
// SAFETY: The hardware_ptr is only accessed during flush(), which requires &mut self.
198+
// The shadow_buffer is a standard Vec which is Send.
199+
unsafe impl Send for DoubleBufferedFrameBuffer {}
200+
201+
// SAFETY: All access to internal state requires &mut self, so there's no data race risk.
202+
// The Mutex wrapper in SHELL_FRAMEBUFFER provides the actual synchronization.
203+
unsafe impl Sync for DoubleBufferedFrameBuffer {}
204+
205+
#[cfg(test)]
206+
mod tests {
207+
use super::*;
208+
209+
#[test]
210+
fn dirty_region_new_is_empty() {
211+
let region = DirtyRegion::new();
212+
assert!(region.is_empty());
213+
}
214+
215+
#[test]
216+
fn dirty_region_mark_expands() {
217+
let mut region = DirtyRegion::new();
218+
region.mark_dirty(2, 4, 8);
219+
assert!(!region.is_empty());
220+
assert_eq!(region.x_start, 4);
221+
assert_eq!(region.x_end, 8);
222+
assert_eq!(region.y_start, 2);
223+
assert_eq!(region.y_end, 3);
224+
}
225+
226+
#[test]
227+
fn dirty_region_mark_unions() {
228+
let mut region = DirtyRegion::new();
229+
region.mark_dirty(2, 4, 8);
230+
region.mark_dirty(1, 2, 6);
231+
assert_eq!(region.x_start, 2);
232+
assert_eq!(region.x_end, 8);
233+
assert_eq!(region.y_start, 1);
234+
assert_eq!(region.y_end, 3);
235+
}
236+
237+
#[test]
238+
fn dirty_region_clear_resets() {
239+
let mut region = DirtyRegion::new();
240+
region.mark_dirty(0, 1, 2);
241+
region.clear();
242+
assert!(region.is_empty());
243+
}
244+
245+
#[test]
246+
fn double_buffer_new_not_dirty() {
247+
let mut buf = [0u8; 100];
248+
let db = DoubleBufferedFrameBuffer::new(buf.as_mut_ptr(), buf.len(), 10, 10);
249+
assert!(!db.dirty);
250+
assert!(db.dirty_region.is_empty());
251+
}
252+
253+
#[test]
254+
fn double_buffer_mark_region_sets_dirty() {
255+
let mut buf = [0u8; 100];
256+
let mut db = DoubleBufferedFrameBuffer::new(buf.as_mut_ptr(), buf.len(), 10, 10);
257+
db.mark_region_dirty(1, 2, 4);
258+
assert!(db.dirty);
259+
assert!(!db.dirty_region.is_empty());
260+
}
261+
262+
#[test]
263+
fn double_buffer_flush_clears_dirty() {
264+
let mut buf = [0u8; 100];
265+
let mut db = DoubleBufferedFrameBuffer::new(buf.as_mut_ptr(), buf.len(), 10, 10);
266+
db.mark_region_dirty(1, 0, 2);
267+
db.flush();
268+
assert!(!db.dirty);
269+
assert!(db.dirty_region.is_empty());
270+
}
271+
272+
#[test]
273+
fn double_buffer_flush_copies_dirty_bytes() {
274+
let mut hw_buf = [0u8; 100];
275+
let mut db = DoubleBufferedFrameBuffer::new(hw_buf.as_mut_ptr(), hw_buf.len(), 10, 10);
276+
277+
let shadow = db.buffer_mut();
278+
shadow[23] = 0xAA;
279+
shadow[24] = 0xBB;
280+
shadow[25] = 0xCC;
281+
282+
db.mark_region_dirty(2, 3, 6);
283+
db.flush();
284+
285+
assert_eq!(hw_buf[23], 0xAA);
286+
assert_eq!(hw_buf[24], 0xBB);
287+
assert_eq!(hw_buf[25], 0xCC);
288+
}
289+
290+
#[test]
291+
fn double_buffer_flush_only_copies_dirty_region() {
292+
let mut hw_buf = [0u8; 100];
293+
let mut db = DoubleBufferedFrameBuffer::new(hw_buf.as_mut_ptr(), hw_buf.len(), 10, 10);
294+
295+
let shadow = db.buffer_mut();
296+
shadow[5] = 0x11;
297+
shadow[23] = 0xAA;
298+
shadow[45] = 0x22;
299+
300+
db.mark_region_dirty(2, 3, 4);
301+
db.flush();
302+
303+
assert_eq!(hw_buf[23], 0xAA);
304+
assert_eq!(hw_buf[5], 0x00, "Row 0 should not be touched");
305+
assert_eq!(hw_buf[45], 0x00, "Row 4 should not be touched");
306+
}
307+
308+
#[test]
309+
fn double_buffer_flush_full_copies_everything() {
310+
let mut hw_buf = [0u8; 100];
311+
let mut db = DoubleBufferedFrameBuffer::new(hw_buf.as_mut_ptr(), hw_buf.len(), 10, 10);
312+
313+
let shadow = db.buffer_mut();
314+
shadow[5] = 0x11;
315+
shadow[50] = 0x22;
316+
shadow[95] = 0x33;
317+
318+
db.flush_full();
319+
320+
assert_eq!(hw_buf[5], 0x11);
321+
assert_eq!(hw_buf[50], 0x22);
322+
assert_eq!(hw_buf[95], 0x33);
323+
}
324+
325+
#[test]
326+
fn double_buffer_coordinate_interpretation() {
327+
let mut hw_buf = [0u8; 100];
328+
let mut db = DoubleBufferedFrameBuffer::new(hw_buf.as_mut_ptr(), hw_buf.len(), 10, 10);
329+
330+
let shadow = db.buffer_mut();
331+
shadow[52] = 0xDE;
332+
shadow[53] = 0xAD;
333+
shadow[54] = 0xBE;
334+
335+
db.mark_region_dirty(5, 2, 5);
336+
db.flush();
337+
338+
assert_eq!(hw_buf[52], 0xDE);
339+
assert_eq!(hw_buf[53], 0xAD);
340+
assert_eq!(hw_buf[54], 0xBE);
341+
assert_eq!(hw_buf[2], 0x00, "Row 0 col 2 should not be touched");
342+
}
343+
344+
#[test]
345+
fn double_buffer_scroll_hardware_up() {
346+
let mut hw_buf = [0u8; 100];
347+
for (idx, byte) in hw_buf.iter_mut().enumerate() {
348+
*byte = idx as u8;
349+
}
350+
351+
let mut db = DoubleBufferedFrameBuffer::new(hw_buf.as_mut_ptr(), hw_buf.len(), 10, 10);
352+
db.scroll_hardware_up(10);
353+
354+
assert_eq!(hw_buf[0], 10);
355+
assert_eq!(hw_buf[9], 19);
356+
assert_eq!(hw_buf[80], 90);
357+
}
358+
}

kernel/src/graphics/mod.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
//! Graphics utilities for the Breenix kernel.
2+
//!
3+
//! Provides framebuffer abstractions used by the kernel graphics stack.
4+
5+
pub mod double_buffer;
6+
pub mod primitives;
7+
8+
pub use double_buffer::DoubleBufferedFrameBuffer;

0 commit comments

Comments
 (0)