Skip to main content

nexcore_softrender/pipeline/
framebuffer.rs

1//! Framebuffer: pixel buffer for CPU rendering
2//!
3//! Stores pixels as ARGB u32. This is the final output — what gets blitted to screen.
4
5use crate::math::Color;
6
7#[derive(Debug, Clone)]
8#[non_exhaustive]
9pub struct Framebuffer {
10    pub width: u32,
11    pub height: u32,
12    /// ARGB packed pixels, row-major: pixel[y * width + x]
13    pub pixels: Vec<u32>,
14}
15
16impl Framebuffer {
17    pub fn new(width: u32, height: u32) -> Self {
18        Self {
19            width,
20            height,
21            pixels: vec![0xFF000000; (width * height) as usize], // opaque black
22        }
23    }
24
25    /// Total pixel count
26    pub fn len(&self) -> usize {
27        self.pixels.len()
28    }
29
30    pub fn is_empty(&self) -> bool {
31        self.pixels.is_empty()
32    }
33
34    /// Clear to a solid color
35    pub fn clear(&mut self, color: Color) {
36        let packed = color.to_argb_u32();
37        self.pixels.fill(packed);
38    }
39
40    /// Set pixel at (x, y). Out of bounds = no-op.
41    pub fn set_pixel(&mut self, x: u32, y: u32, color: Color) {
42        if x < self.width && y < self.height {
43            let idx = (y * self.width + x) as usize;
44            self.pixels[idx] = color.to_argb_u32();
45        }
46    }
47
48    /// Set pixel with alpha blending over existing pixel
49    pub fn blend_pixel(&mut self, x: u32, y: u32, src: Color) {
50        if x < self.width && y < self.height {
51            let idx = (y * self.width + x) as usize;
52            let dst = Color::from_argb_u32(self.pixels[idx]);
53            self.pixels[idx] = src.alpha_over(dst).to_argb_u32();
54        }
55    }
56
57    /// Get pixel color at (x, y). Out of bounds = transparent black.
58    pub fn get_pixel(&self, x: u32, y: u32) -> Color {
59        if x < self.width && y < self.height {
60            let idx = (y * self.width + x) as usize;
61            Color::from_argb_u32(self.pixels[idx])
62        } else {
63            Color::TRANSPARENT
64        }
65    }
66
67    /// Raw pixel data as RGBA bytes (for PNG export or display)
68    pub fn to_rgba_bytes(&self) -> Vec<u8> {
69        let mut bytes = Vec::with_capacity(self.pixels.len() * 4);
70        for &pixel in &self.pixels {
71            bytes.push(((pixel >> 16) & 0xFF) as u8); // R
72            bytes.push(((pixel >> 8) & 0xFF) as u8); // G
73            bytes.push((pixel & 0xFF) as u8); // B
74            bytes.push(((pixel >> 24) & 0xFF) as u8); // A
75        }
76        bytes
77    }
78
79    /// Raw pixel data as BGRA bytes (for some windowing systems)
80    pub fn to_bgra_bytes(&self) -> Vec<u8> {
81        let mut bytes = Vec::with_capacity(self.pixels.len() * 4);
82        for &pixel in &self.pixels {
83            bytes.push((pixel & 0xFF) as u8); // B
84            bytes.push(((pixel >> 8) & 0xFF) as u8); // G
85            bytes.push(((pixel >> 16) & 0xFF) as u8); // R
86            bytes.push(((pixel >> 24) & 0xFF) as u8); // A
87        }
88        bytes
89    }
90
91    /// Blit another framebuffer onto this one at (dx, dy)
92    pub fn blit(&mut self, src: &Framebuffer, dx: i32, dy: i32) {
93        for sy in 0..src.height {
94            let ty = dy + sy as i32;
95            if ty < 0 || ty >= self.height as i32 {
96                continue;
97            }
98            for sx in 0..src.width {
99                let tx = dx + sx as i32;
100                if tx < 0 || tx >= self.width as i32 {
101                    continue;
102                }
103                let src_color = src.get_pixel(sx, sy);
104                self.blend_pixel(tx as u32, ty as u32, src_color);
105            }
106        }
107    }
108}
109
110// ============================================================================
111// Tests
112// ============================================================================
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[test]
119    fn new_framebuffer_is_black() {
120        let fb = Framebuffer::new(100, 100);
121        assert_eq!(fb.len(), 10000);
122        let c = fb.get_pixel(50, 50);
123        assert!((c.r - 0.0).abs() < 0.01);
124        assert!((c.a - 1.0).abs() < 0.01);
125    }
126
127    #[test]
128    fn set_and_get_pixel() {
129        let mut fb = Framebuffer::new(10, 10);
130        fb.set_pixel(5, 5, Color::RED);
131        let c = fb.get_pixel(5, 5);
132        assert!((c.r - 1.0).abs() < 0.01);
133        assert!((c.g - 0.0).abs() < 0.01);
134    }
135
136    #[test]
137    fn out_of_bounds_noop() {
138        let mut fb = Framebuffer::new(10, 10);
139        fb.set_pixel(100, 100, Color::RED); // no panic
140        let c = fb.get_pixel(100, 100);
141        assert!((c.a - 0.0).abs() < 0.01); // transparent
142    }
143
144    #[test]
145    fn clear_to_color() {
146        let mut fb = Framebuffer::new(10, 10);
147        fb.clear(Color::BLUE);
148        let c = fb.get_pixel(0, 0);
149        assert!((c.b - 1.0).abs() < 0.01);
150    }
151
152    #[test]
153    fn rgba_bytes_length() {
154        let fb = Framebuffer::new(8, 8);
155        assert_eq!(fb.to_rgba_bytes().len(), 8 * 8 * 4);
156    }
157
158    #[test]
159    fn blit_small_onto_large() {
160        let mut dst = Framebuffer::new(20, 20);
161        dst.clear(Color::BLACK);
162
163        let mut src = Framebuffer::new(5, 5);
164        src.clear(Color::RED);
165
166        dst.blit(&src, 10, 10);
167
168        // Inside blit region
169        let c = dst.get_pixel(12, 12);
170        assert!((c.r - 1.0).abs() < 0.01);
171
172        // Outside blit region
173        let c2 = dst.get_pixel(0, 0);
174        assert!((c2.r - 0.0).abs() < 0.01);
175    }
176}