Skip to main content

strat9_kernel/framebuffer/
mod.rs

1use alloc::vec::Vec;
2
3pub mod generic;
4pub mod gpu;
5
6#[cfg(test)]
7pub mod tests;
8
9#[cfg(target_arch = "x86_64")]
10pub mod x86;
11
12#[cfg(target_arch = "aarch64")]
13pub mod aarch64;
14
15// Shared RgbColor type used by all framebuffer/VGA code.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct RgbColor {
18    pub r: u8,
19    pub g: u8,
20    pub b: u8,
21}
22
23impl RgbColor {
24    /// Creates a new instance.
25    pub const fn new(r: u8, g: u8, b: u8) -> Self {
26        Self { r, g, b }
27    }
28
29    pub const BLACK: Self = Self::new(0x00, 0x00, 0x00);
30    pub const WHITE: Self = Self::new(0xFF, 0xFF, 0xFF);
31    pub const RED: Self = Self::new(0xFF, 0x00, 0x00);
32    pub const GREEN: Self = Self::new(0x00, 0xFF, 0x00);
33    pub const BLUE: Self = Self::new(0x00, 0x00, 0xFF);
34    pub const CYAN: Self = Self::new(0x00, 0xFF, 0xFF);
35    pub const MAGENTA: Self = Self::new(0xFF, 0x00, 0xFF);
36    pub const YELLOW: Self = Self::new(0xFF, 0xFF, 0x00);
37    pub const LIGHT_GREY: Self = Self::new(0xAA, 0xAA, 0xAA);
38}
39
40// Shared dirty-rect tracking used by VGA writer and video framebuffer.
41#[derive(Clone, Copy, Debug, Default)]
42pub struct DirtyRect {
43    pub x0: u32,
44    pub y0: u32,
45    pub x1: u32,
46    pub y1: u32,
47}
48
49impl DirtyRect {
50    pub const fn empty() -> Self {
51        Self {
52            x0: 0,
53            y0: 0,
54            x1: 0,
55            y1: 0,
56        }
57    }
58
59    pub fn is_valid(self) -> bool {
60        self.x0 < self.x1 && self.y0 < self.y1
61    }
62
63    pub fn width(self) -> u32 {
64        self.x1.saturating_sub(self.x0)
65    }
66
67    pub fn height(self) -> u32 {
68        self.y1.saturating_sub(self.y0)
69    }
70
71    pub fn include(&mut self, x: u32, y: u32, w: u32, h: u32) {
72        if w == 0 || h == 0 {
73            return;
74        }
75        let nx1 = x.saturating_add(w);
76        let ny1 = y.saturating_add(h);
77        if !self.is_valid() {
78            self.x0 = x;
79            self.y0 = y;
80            self.x1 = nx1;
81            self.y1 = ny1;
82        } else {
83            self.x0 = self.x0.min(x);
84            self.y0 = self.y0.min(y);
85            self.x1 = self.x1.max(nx1);
86            self.y1 = self.y1.max(ny1);
87        }
88    }
89
90    pub fn overlaps(self, other: DirtyRect) -> bool {
91        self.x0 < other.x1 && self.x1 > other.x0 && self.y0 < other.y1 && self.y1 > other.y0
92    }
93
94    /// Coalesce adjacent glyphs/rows without covering extra clean pixels.
95    fn adjacent(self, other: DirtyRect) -> bool {
96        (self.y0 == other.y0 && self.y1 == other.y1 && (self.x1 == other.x0 || other.x1 == self.x0))
97            || (self.x0 == other.x0
98                && self.x1 == other.x1
99                && (self.y1 == other.y0 || other.y1 == self.y0))
100    }
101}
102
103pub const MAX_DIRTY_RECTS: usize = 8;
104
105#[derive(Clone, Copy)]
106pub struct DirtyRectSet {
107    pub rects: [DirtyRect; MAX_DIRTY_RECTS],
108    pub len: usize,
109}
110
111impl DirtyRectSet {
112    pub const fn empty() -> Self {
113        Self {
114            rects: [DirtyRect::empty(); MAX_DIRTY_RECTS],
115            len: 0,
116        }
117    }
118
119    pub fn clear(&mut self) {
120        self.len = 0;
121    }
122
123    pub fn include(&mut self, x: u32, y: u32, w: u32, h: u32) {
124        if w == 0 || h == 0 {
125            return;
126        }
127        let mut next = DirtyRect::empty();
128        next.include(x, y, w, h);
129
130        let mut idx = 0;
131        while idx < self.len {
132            let cur = self.rects[idx];
133            if !cur.is_valid() {
134                idx += 1;
135                continue;
136            }
137            if cur.overlaps(next) || cur.adjacent(next) {
138                next.include(cur.x0, cur.y0, cur.width(), cur.height());
139                self.rects[idx] = self.rects[self.len - 1];
140                self.len -= 1;
141                idx = 0;
142                continue;
143            }
144            idx += 1;
145        }
146
147        if self.len < MAX_DIRTY_RECTS {
148            self.rects[self.len] = next;
149            self.len += 1;
150            return;
151        }
152        self.rects[0].include(next.x0, next.y0, next.width(), next.height());
153    }
154
155    pub fn iter(&self) -> DirtyRectIter<'_> {
156        DirtyRectIter { set: self, idx: 0 }
157    }
158}
159
160pub struct DirtyRectIter<'a> {
161    set: &'a DirtyRectSet,
162    idx: usize,
163}
164
165impl<'a> Iterator for DirtyRectIter<'a> {
166    type Item = &'a DirtyRect;
167
168    fn next(&mut self) -> Option<Self::Item> {
169        while self.idx < self.set.len {
170            let r = &self.set.rects[self.idx];
171            self.idx += 1;
172            if r.is_valid() {
173                return Some(r);
174            }
175        }
176        None
177    }
178}
179
180// Type definitions for our framebuffer operations
181pub type FnFill = unsafe fn(dst: *mut u32, color: u32, count: usize);
182pub type FnBlit = unsafe fn(dst: *mut u32, src: *const u32, count: usize);
183pub type FnBlend = unsafe fn(dst: *mut u32, src: *const u32, alpha: u8, count: usize);
184pub type FnConvert = unsafe fn(dst: *mut u32, src: *const u8, count: usize);
185
186#[derive(Clone, Copy)]
187pub struct FramebufferOps {
188    pub fill: FnFill,
189    pub blit: FnBlit,
190    pub blend: FnBlend,
191    pub convert: FnConvert,
192}
193
194// ---------------------------------------------------------------------------
195// CanvasBuffer : shared double-buffered framebuffer canvas
196// ---------------------------------------------------------------------------
197
198/// A raw framebuffer canvas with optional double buffering.
199///
200/// Encapsulates the HW framebuffer pointer, optional back-buffer (always
201/// 32bpp), dirty-region tracking, SIMD-accelerated pixel ops, and a
202/// `present()` that flushes dirty regions to the real hardware.
203///
204/// Used by both the VGA text-mode writer (`VgaWriter`) and the raw
205/// hardware video driver (`hardware::video::Framebuffer`), eliminating
206/// the duplicated `present()` / dirty-tracking logic between them.
207pub struct CanvasBuffer {
208    /// Pointer to the start of the HW framebuffer (HHDM-mapped virtual).
209    pub addr: *mut u8,
210    /// Width in pixels.
211    pub width: usize,
212    /// Height in pixels.
213    pub height: usize,
214    /// Bytes per row (stride / pitch).
215    pub pitch: usize,
216    /// Bits per pixel (24 or 32).
217    pub bpp: u16,
218    /// Optional back-buffer (always 32bpp / `u32` per pixel).
219    pub back_buffer: Option<Vec<u32>>,
220    /// If `true`, draw operations write to `back_buffer` instead of HW.
221    pub draw_to_back: bool,
222    /// Set of dirty rectangles pending a `present()`.
223    pub dirty: DirtyRectSet,
224    /// If `true`, dirty rectangles are tracked.
225    pub track_dirty: bool,
226    /// Whether a `present()` has been requested.
227    pub present_pending: bool,
228    /// Tick of the last `present()` (for rate-limiting).
229    pub last_present_tick: u64,
230    /// SIMD-accelerated pixel ops (fill, blit, blend, convert).
231    pub ops: FramebufferOps,
232    /// Reusable row buffer for the 24bpp present path (S5): allocated once
233    /// at the largest row seen instead of one Vec per region per present.
234    pub present_row_buf: Option<Vec<u8>>,
235}
236
237unsafe impl Send for CanvasBuffer {}
238
239impl CanvasBuffer {
240    /// Enable double-buffering by allocating and syncing a back-buffer.
241    pub fn enable_back_buffer(&mut self) -> bool {
242        if self.back_buffer.is_some() {
243            return true;
244        }
245        let total = self.width.saturating_mul(self.height);
246        if total == 0 || self.addr.is_null() {
247            return false;
248        }
249        let mut buf = alloc::vec![0u32; total];
250        // Sync current HW content into back-buffer.
251        if self.bpp == 32 {
252            unsafe {
253                for y in 0..self.height {
254                    core::ptr::copy_nonoverlapping(
255                        self.addr.add(y * self.pitch),
256                        buf.as_mut_ptr().add(y * self.width) as *mut u8,
257                        self.width * 4,
258                    );
259                }
260            }
261        } else {
262            for y in 0..self.height {
263                for x in 0..self.width {
264                    buf[y * self.width + x] = self.read_hw_pixel(x, y);
265                }
266            }
267        }
268        self.back_buffer = Some(buf);
269        self.draw_to_back = true;
270        self.track_dirty = true;
271        self.dirty.clear();
272        true
273    }
274
275    /// Disable double-buffering, optionally presenting first.
276    pub fn disable_back_buffer(&mut self, do_present: bool) {
277        if do_present {
278            self.present();
279        }
280        self.draw_to_back = false;
281        self.track_dirty = false;
282        self.dirty.clear();
283    }
284
285    /// Byte offset in HW memory for pixel `(x, y)`.
286    #[inline]
287    pub fn pixel_offset(&self, x: usize, y: usize) -> Option<usize> {
288        if x >= self.width || y >= self.height {
289            return None;
290        }
291        let bytes_pp = (self.bpp / 8) as usize;
292        Some(
293            y.checked_mul(self.pitch)?
294                .checked_add(x.checked_mul(bytes_pp)?)?,
295        )
296    }
297
298    /// Read a packed pixel directly from HW memory.
299    pub fn read_hw_pixel(&self, x: usize, y: usize) -> u32 {
300        let Some(off) = self.pixel_offset(x, y) else {
301            return 0;
302        };
303        unsafe {
304            match self.bpp {
305                32 => core::ptr::read_volatile(self.addr.add(off) as *const u32),
306                24 => {
307                    let b0 = core::ptr::read_volatile(self.addr.add(off)) as u32;
308                    let b1 = core::ptr::read_volatile(self.addr.add(off + 1)) as u32;
309                    let b2 = core::ptr::read_volatile(self.addr.add(off + 2)) as u32;
310                    b0 | (b1 << 8) | (b2 << 16)
311                }
312                _ => 0,
313            }
314        }
315    }
316
317    /// Write a packed pixel to HW memory.
318    pub fn write_hw_pixel(&mut self, x: usize, y: usize, color: u32) {
319        let Some(off) = self.pixel_offset(x, y) else {
320            return;
321        };
322        unsafe {
323            match self.bpp {
324                32 => {
325                    core::ptr::write_volatile(self.addr.add(off) as *mut u32, color);
326                }
327                24 => {
328                    core::ptr::write_volatile(self.addr.add(off), (color & 0xFF) as u8);
329                    core::ptr::write_volatile(self.addr.add(off + 1), ((color >> 8) & 0xFF) as u8);
330                    core::ptr::write_volatile(self.addr.add(off + 2), ((color >> 16) & 0xFF) as u8);
331                }
332                _ => {}
333            }
334        }
335    }
336
337    /// Read pixel `(x, y)` : from back-buffer if available, else HW.
338    pub fn read_pixel(&self, x: usize, y: usize) -> u32 {
339        if x >= self.width || y >= self.height {
340            return 0;
341        }
342        if self.draw_to_back {
343            if let Some(buf) = self.back_buffer.as_ref() {
344                return buf[y * self.width + x];
345            }
346        }
347        self.read_hw_pixel(x, y)
348    }
349
350    /// Write pixel `(x, y)` : to back-buffer if available, else HW.
351    ///
352    /// Pure write (S4): this does NOT touch the dirty set. Marking per pixel
353    /// cost an O(dirty-set) scan on every call and no caller relied on it;
354    /// callers that need present tracking mark rectangles explicitly via
355    /// [`DirtyRectSet::include`] or go through [`Self::fill_rect`].
356    pub fn write_pixel(&mut self, x: usize, y: usize, color: u32) {
357        if x >= self.width || y >= self.height {
358            return;
359        }
360        if self.draw_to_back {
361            if let Some(buf) = self.back_buffer.as_mut() {
362                buf[y * self.width + x] = color;
363                return;
364            }
365        }
366        self.write_hw_pixel(x, y, color);
367    }
368
369    /// Fill a rectangle with a packed colour.
370    pub fn fill_rect(&mut self, x: usize, y: usize, w: usize, h: usize, color: u32) {
371        if w == 0 || h == 0 {
372            return;
373        }
374        let x_end = core::cmp::min(x.saturating_add(w), self.width);
375        let y_end = core::cmp::min(y.saturating_add(h), self.height);
376        if x_end <= x || y_end <= y {
377            return;
378        }
379        let fw = self.width;
380        let sw = x_end - x;
381        let sh = y_end - y;
382
383        if self.draw_to_back {
384            if let Some(buf) = self.back_buffer.as_mut() {
385                for py in y..y_end {
386                    let start = py * fw + x;
387                    buf[start..start + sw].fill(color);
388                }
389                self.dirty.include(x as u32, y as u32, sw as u32, sh as u32);
390                return;
391            }
392        }
393
394        if self.bpp == 32 {
395            for py in y..y_end {
396                if let Some(off) = py
397                    .checked_mul(self.pitch)
398                    .and_then(|r| r.checked_add(x * 4))
399                {
400                    unsafe {
401                        let ptr = self.addr.add(off) as *mut u32;
402                        (self.ops.fill)(ptr, color, sw);
403                    }
404                }
405            }
406        } else {
407            for py in y..y_end {
408                for px in x..x_end {
409                    self.write_hw_pixel(px, py, color);
410                }
411            }
412        }
413        if self.track_dirty {
414            self.dirty.include(x as u32, y as u32, sw as u32, sh as u32);
415        }
416    }
417
418    /// Flush dirty back-buffer regions to the hardware framebuffer.
419    ///
420    /// Uses SIMD-accelerated `ops.blit` when the back-buffer's row stride
421    /// matches the frame-buffer's stride, falling back to row-by-row copies.
422    pub fn present(&mut self) {
423        let Some(buf) = self.back_buffer.as_ref() else {
424            return;
425        };
426        if self.track_dirty && self.dirty.len == 0 {
427            return;
428        }
429        let buf_ptr = buf.as_ptr();
430        let pitch = self.pitch;
431        let fb_addr = self.addr;
432
433        // Collect regions to present.
434        let mut regions: [(u32, u32, u32, u32); MAX_DIRTY_RECTS] = [(0, 0, 0, 0); MAX_DIRTY_RECTS];
435        let mut region_count = 0usize;
436
437        if self.track_dirty {
438            for r in self.dirty.iter() {
439                if region_count < MAX_DIRTY_RECTS {
440                    regions[region_count] = (r.x0, r.y0, r.width(), r.height());
441                    region_count += 1;
442                }
443            }
444        } else {
445            regions[0] = (0, 0, self.width as u32, self.height as u32);
446            region_count = 1;
447        }
448
449        let stride_pixels_u32 = pitch / 4; // stride in u32 units
450        for i in 0..region_count {
451            let (rx, ry, rw, rh) = regions[i];
452            if rw == 0 || rh == 0 {
453                continue;
454            }
455            let (rx, ry, rw, rh) = (rx as usize, ry as usize, rw as usize, rh as usize);
456
457            // Fast path: row stride matches, blit contiguous block.
458            if self.bpp == 32 {
459                if self.width == stride_pixels_u32 && rx == 0 && rw == self.width {
460                    let off = ry * self.width + rx;
461                    let dst_off = ry * pitch + rx * 4;
462                    unsafe {
463                        let src = buf_ptr.add(off) as *const u8;
464                        (self.ops.blit)(
465                            fb_addr.add(dst_off) as *mut u32,
466                            src as *const u32,
467                            rw * rh,
468                        );
469                        // (ops.blit) uses copy_nonoverlapping internally
470                    }
471                } else {
472                    for row in 0..rh {
473                        let src_off = (ry + row) * self.width + rx;
474                        let dst_off = (ry + row) * pitch + rx * 4;
475                        unsafe {
476                            let src = buf_ptr.add(src_off) as *const u32;
477                            (self.ops.blit)(fb_addr.add(dst_off) as *mut u32, src, rw);
478                        }
479                    }
480                }
481            } else {
482                // 24bpp fallback: convert row-by-row into a reusable buffer
483                // (S5: no per-region allocation).
484                let row_bytes = rw * 3;
485                let row_buf = self
486                    .present_row_buf
487                    .get_or_insert_with(|| Vec::with_capacity(row_bytes));
488                row_buf.clear();
489                row_buf.resize(row_bytes, 0);
490                for row in 0..rh {
491                    let src_row = (ry + row) * self.width + rx;
492                    for x in 0..rw {
493                        let packed = unsafe { *buf_ptr.add(src_row + x) };
494                        let off = x * 3;
495                        row_buf[off] = packed as u8;
496                        row_buf[off + 1] = (packed >> 8) as u8;
497                        row_buf[off + 2] = (packed >> 16) as u8;
498                    }
499                    let dst_off = (ry + row) * pitch + rx * 3;
500                    unsafe {
501                        core::ptr::copy_nonoverlapping(
502                            row_buf.as_ptr(),
503                            fb_addr.add(dst_off),
504                            row_bytes,
505                        );
506                    }
507                }
508            }
509        }
510
511        self.dirty.clear();
512        self.present_pending = false;
513    }
514
515    /// Request a present (throttled).
516    pub fn request_present(&mut self) {
517        if !self.draw_to_back {
518            return;
519        }
520        self.present_pending = true;
521    }
522
523    /// Present if a request is pending and the throttle delay has elapsed.
524    pub fn present_if_due(&mut self, force: bool, now: u64) {
525        if !self.present_pending || !self.draw_to_back {
526            return;
527        }
528        if force || now.saturating_sub(self.last_present_tick) >= PRESENT_MIN_TICKS {
529            self.last_present_tick = now;
530            self.present();
531        }
532    }
533}
534
535/// Minimum scheduler ticks between two throttled presents, shared by the
536/// VGA console writer and the video driver.
537///
538/// `TIMER_HZ` is 100 (one tick = 10 ms): allow one present per tick so
539/// pending console work is eligible within 10 ms. Paths that pace themselves
540/// (the userspace compositor, `Framebuffer::swap_buffers`) bypass this gate.
541pub const PRESENT_MIN_TICKS: u64 = 1;
542
543impl FramebufferOps {
544    pub fn detect() -> Self {
545        #[cfg(target_arch = "x86_64")]
546        {
547            return x86::detect_and_init_ops();
548        }
549
550        #[cfg(target_arch = "aarch64")]
551        {
552            // ARM CPUID detection for NEON could be done here,
553            // but for now we fallback to generic or neon if compiled with it.
554            return aarch64::detect_and_init_ops();
555        }
556
557        #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
558        {
559            Self {
560                fill: generic::fill_generic,
561                blit: generic::blit_generic,
562                blend: generic::blend_generic,
563                convert: generic::convert_bgr_to_argb_generic,
564            }
565        }
566    }
567}