Skip to main content

oxiui_render_soft/
framebuffer.rs

1//! CPU pixel framebuffer in `0xAARRGGBB` (non-premultiplied) format.
2
3use crate::headless::RgbaBuffer;
4use oxiui_core::Color;
5
6/// Pack an [`oxiui_core::Color`] into a `0xAARRGGBB` `u32`.
7pub fn pack(c: &Color) -> u32 {
8    let Color(r, g, b, a) = *c;
9    ((a as u32) << 24) | ((r as u32) << 16) | ((g as u32) << 8) | (b as u32)
10}
11
12/// Unpack a `0xAARRGGBB` `u32` into `(r, g, b, a)` components.
13pub fn unpack(px: u32) -> (u8, u8, u8, u8) {
14    let a = ((px >> 24) & 0xFF) as u8;
15    let r = ((px >> 16) & 0xFF) as u8;
16    let g = ((px >> 8) & 0xFF) as u8;
17    let b = (px & 0xFF) as u8;
18    (r, g, b, a)
19}
20
21/// Pack raw `(r, g, b, a)` components into `0xAARRGGBB`.
22pub fn pack_rgba(r: u8, g: u8, b: u8, a: u8) -> u32 {
23    ((a as u32) << 24) | ((r as u32) << 16) | ((g as u32) << 8) | (b as u32)
24}
25
26/// A CPU-side pixel buffer storing `0xAARRGGBB` values row-major.
27#[derive(Clone, Debug)]
28pub struct Framebuffer {
29    width: u32,
30    height: u32,
31    pixels: Vec<u32>,
32}
33
34impl Framebuffer {
35    /// Allocate a transparent (all-zero) framebuffer of the given size.
36    pub fn new(width: u32, height: u32) -> Self {
37        Self {
38            width,
39            height,
40            pixels: vec![0u32; (width * height) as usize],
41        }
42    }
43
44    /// Allocate a framebuffer pre-filled with `color`.
45    pub fn with_fill(width: u32, height: u32, color: Color) -> Self {
46        Self {
47            width,
48            height,
49            pixels: vec![pack(&color); (width * height) as usize],
50        }
51    }
52
53    /// Width in pixels.
54    pub fn width(&self) -> u32 {
55        self.width
56    }
57
58    /// Height in pixels.
59    pub fn height(&self) -> u32 {
60        self.height
61    }
62
63    /// Borrow the raw pixel slice (`0xAARRGGBB`, row-major).
64    pub fn pixels(&self) -> &[u32] {
65        &self.pixels
66    }
67
68    /// Mutably borrow the raw pixel slice.
69    pub fn pixels_mut(&mut self) -> &mut [u32] {
70        &mut self.pixels
71    }
72
73    /// Fill the entire buffer with `color`, discarding existing content.
74    pub fn clear(&mut self, color: Color) {
75        let v = pack(&color);
76        for px in &mut self.pixels {
77            *px = v;
78        }
79    }
80
81    #[inline]
82    fn index(&self, x: u32, y: u32) -> Option<usize> {
83        if x < self.width && y < self.height {
84            Some((y * self.width + x) as usize)
85        } else {
86            None
87        }
88    }
89
90    /// Get the packed pixel at `(x, y)`, or `None` if out of bounds.
91    pub fn get(&self, x: u32, y: u32) -> Option<u32> {
92        self.index(x, y).map(|i| self.pixels[i])
93    }
94
95    /// Get the `(r, g, b, a)` components at `(x, y)`, or `None` if out of bounds.
96    pub fn get_rgba(&self, x: u32, y: u32) -> Option<(u8, u8, u8, u8)> {
97        self.get(x, y).map(unpack)
98    }
99
100    /// Set the packed pixel at `(x, y)` (overwrites; no blending). Out-of-bounds
101    /// writes are ignored.
102    pub fn set(&mut self, x: u32, y: u32, px: u32) {
103        if let Some(i) = self.index(x, y) {
104            self.pixels[i] = px;
105        }
106    }
107
108    /// Borrow row `y` as a slice, or `None` if out of bounds.
109    pub fn row(&self, y: u32) -> Option<&[u32]> {
110        if y < self.height {
111            let start = (y * self.width) as usize;
112            Some(&self.pixels[start..start + self.width as usize])
113        } else {
114            None
115        }
116    }
117
118    /// Composite `src` over the existing pixel at `(x, y)` using straight-alpha
119    /// source-over blending. Out-of-bounds writes are ignored.
120    pub fn blend(&mut self, x: u32, y: u32, src: u32) {
121        let i = match self.index(x, y) {
122            Some(i) => i,
123            None => return,
124        };
125        let (sr, sg, sb, sa) = unpack(src);
126        if sa == 0 {
127            return;
128        }
129        if sa == 255 {
130            self.pixels[i] = src;
131            return;
132        }
133        let (dr, dg, db, da) = unpack(self.pixels[i]);
134        let sa_f = sa as f32 / 255.0;
135        let da_f = da as f32 / 255.0;
136        let out_a = sa_f + da_f * (1.0 - sa_f);
137        if out_a <= 0.0 {
138            self.pixels[i] = 0;
139            return;
140        }
141        let blend_ch = |s: u8, d: u8| -> u8 {
142            let s = s as f32 / 255.0;
143            let d = d as f32 / 255.0;
144            let v = (s * sa_f + d * da_f * (1.0 - sa_f)) / out_a;
145            (v.clamp(0.0, 1.0) * 255.0).round() as u8
146        };
147        let r = blend_ch(sr, dr);
148        let g = blend_ch(sg, dg);
149        let b = blend_ch(sb, db);
150        let a = (out_a.clamp(0.0, 1.0) * 255.0).round() as u8;
151        self.pixels[i] = pack_rgba(r, g, b, a);
152    }
153
154    /// Blend a colour `c` with an extra coverage factor `coverage` in `[0, 1]`
155    /// (used for anti-aliased edges). The colour's own alpha is multiplied by
156    /// `coverage` before compositing.
157    pub fn blend_coverage(&mut self, x: u32, y: u32, c: &Color, coverage: f32) {
158        let cov = coverage.clamp(0.0, 1.0);
159        if cov <= 0.0 {
160            return;
161        }
162        let Color(r, g, b, a) = *c;
163        let a = (a as f32 * cov).round() as u8;
164        self.blend(x, y, pack_rgba(r, g, b, a));
165    }
166
167    /// Convert to an [`RgbaBuffer`] (R, G, B, A bytes) suitable for PNG export.
168    pub fn to_rgba_buffer(&self) -> RgbaBuffer {
169        let mut data = Vec::with_capacity(self.pixels.len() * 4);
170        for &px in &self.pixels {
171            let (r, g, b, a) = unpack(px);
172            data.extend_from_slice(&[r, g, b, a]);
173        }
174        RgbaBuffer {
175            width: self.width,
176            height: self.height,
177            data,
178        }
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185
186    #[test]
187    fn pack_unpack_roundtrip() {
188        let c = Color(10, 20, 30, 200);
189        let packed = pack(&c);
190        assert_eq!(unpack(packed), (10, 20, 30, 200));
191    }
192
193    #[test]
194    fn fill_and_get() {
195        let fb = Framebuffer::with_fill(4, 4, Color(255, 0, 0, 255));
196        assert_eq!(fb.get_rgba(0, 0), Some((255, 0, 0, 255)));
197        assert_eq!(fb.get_rgba(3, 3), Some((255, 0, 0, 255)));
198        assert_eq!(fb.get(4, 4), None);
199    }
200
201    #[test]
202    fn opaque_blend_overwrites() {
203        let mut fb = Framebuffer::with_fill(2, 2, Color(0, 0, 0, 255));
204        fb.blend(0, 0, pack(&Color(255, 255, 255, 255)));
205        assert_eq!(fb.get_rgba(0, 0), Some((255, 255, 255, 255)));
206    }
207
208    #[test]
209    fn half_alpha_blend() {
210        // 50% white over opaque black => mid-grey, fully opaque.
211        let mut fb = Framebuffer::with_fill(1, 1, Color(0, 0, 0, 255));
212        fb.blend(0, 0, pack(&Color(255, 255, 255, 128)));
213        let (r, g, b, a) = fb.get_rgba(0, 0).expect("pixel");
214        assert!((120..=135).contains(&r), "r={r}");
215        assert_eq!(g, r);
216        assert_eq!(b, r);
217        assert_eq!(a, 255);
218    }
219
220    #[test]
221    fn transparent_blend_is_noop() {
222        let mut fb = Framebuffer::with_fill(1, 1, Color(10, 20, 30, 255));
223        fb.blend(0, 0, pack(&Color(255, 255, 255, 0)));
224        assert_eq!(fb.get_rgba(0, 0), Some((10, 20, 30, 255)));
225    }
226
227    #[test]
228    fn coverage_scales_alpha() {
229        let mut fb = Framebuffer::with_fill(1, 1, Color(0, 0, 0, 255));
230        // Full-opaque white at 0 coverage = no change.
231        fb.blend_coverage(0, 0, &Color(255, 255, 255, 255), 0.0);
232        assert_eq!(fb.get_rgba(0, 0), Some((0, 0, 0, 255)));
233        // Full coverage = full overwrite.
234        fb.blend_coverage(0, 0, &Color(255, 255, 255, 255), 1.0);
235        assert_eq!(fb.get_rgba(0, 0), Some((255, 255, 255, 255)));
236    }
237
238    #[test]
239    fn to_rgba_buffer_layout() {
240        let fb = Framebuffer::with_fill(2, 1, Color(1, 2, 3, 4));
241        let rgba = fb.to_rgba_buffer();
242        assert_eq!(rgba.width, 2);
243        assert_eq!(rgba.data, vec![1, 2, 3, 4, 1, 2, 3, 4]);
244    }
245}