Skip to main content

stipple_render/
surface.rs

1use stipple_geometry::{PhysicalSize, Rect};
2
3/// A CPU pixel buffer in straight (non-premultiplied) RGBA8, row-major,
4/// tightly packed (`stride == width * 4`).
5///
6/// This is the payload handed to a [`Surface`] for display. It is what the
7/// software backend produces today; a future GPU backend may bypass it.
8#[derive(Clone, Debug)]
9pub struct Pixmap {
10    size: PhysicalSize,
11    data: Vec<u8>,
12}
13
14impl Pixmap {
15    /// Allocate a fully transparent pixmap of `size`.
16    pub fn new(size: PhysicalSize) -> Self {
17        Self {
18            size,
19            data: vec![0u8; size.pixel_count() as usize * 4],
20        }
21    }
22
23    /// Wrap existing RGBA8 bytes. Panics if `data.len() != width*height*4`.
24    pub fn from_rgba8(size: PhysicalSize, data: Vec<u8>) -> Self {
25        assert_eq!(
26            data.len(),
27            size.pixel_count() as usize * 4,
28            "pixmap byte length must equal width*height*4"
29        );
30        Self { size, data }
31    }
32
33    #[inline]
34    pub fn size(&self) -> PhysicalSize {
35        self.size
36    }
37
38    /// Bytes per row.
39    #[inline]
40    pub fn stride(&self) -> usize {
41        self.size.width as usize * 4
42    }
43
44    #[inline]
45    pub fn as_bytes(&self) -> &[u8] {
46        &self.data
47    }
48
49    #[inline]
50    pub fn as_bytes_mut(&mut self) -> &mut [u8] {
51        &mut self.data
52    }
53
54    /// Copy `src` into this pixmap with its top-left at `(dst_x, dst_y)`,
55    /// clipped to this pixmap's bounds. A straight row-by-row replace (no alpha
56    /// blend) — used to composite an opaque area-repaint render (see
57    /// [`SoftwareRenderer::render_region`]) into the retained full-window buffer.
58    ///
59    /// [`SoftwareRenderer::render_region`]: crate::SoftwareRenderer::render_region
60    pub fn blit(&mut self, src: &Pixmap, dst_x: u32, dst_y: u32) {
61        let dst_w = self.size.width;
62        let dst_h = self.size.height;
63        // Rows/cols of `src` that land inside this pixmap.
64        let copy_w = src.size.width.min(dst_w.saturating_sub(dst_x));
65        let copy_h = src.size.height.min(dst_h.saturating_sub(dst_y));
66        if copy_w == 0 || copy_h == 0 {
67            return;
68        }
69        let (src_stride, dst_stride) = (src.stride(), self.stride());
70        let row_bytes = copy_w as usize * 4;
71        for row in 0..copy_h as usize {
72            let s = row * src_stride;
73            let d = (dst_y as usize + row) * dst_stride + dst_x as usize * 4;
74            self.data[d..d + row_bytes].copy_from_slice(&src.data[s..s + row_bytes]);
75        }
76    }
77
78    /// The four bytes `[r, g, b, a]` at `(x, y)`, or `None` if out of bounds.
79    pub fn pixel(&self, x: u32, y: u32) -> Option<[u8; 4]> {
80        if x >= self.size.width || y >= self.size.height {
81            return None;
82        }
83        let i = y as usize * self.stride() + x as usize * 4;
84        Some([
85            self.data[i],
86            self.data[i + 1],
87            self.data[i + 2],
88            self.data[i + 3],
89        ])
90    }
91}
92
93/// A presentable destination owned by the platform layer (a window's drawable,
94/// a canvas, a layer-backed view, …).
95///
96/// This trait is the **GPU-readiness seam**: the software backend presents a
97/// [`Pixmap`] by blitting; a future GPU backend can implement the same trait
98/// by uploading or compositing directly. `damage` lists the regions (in
99/// physical pixels) that actually changed since the last present — an empty
100/// slice means "assume everything changed."
101pub trait Surface {
102    /// Resize the backing store to `size` physical pixels.
103    fn resize(&mut self, size: PhysicalSize);
104
105    /// The current backing-store size in physical pixels.
106    fn size(&self) -> PhysicalSize;
107
108    /// Present `pixmap`, optionally limited to the `damage` regions.
109    fn present(&mut self, pixmap: &Pixmap, damage: &[Rect]);
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn pixel_access_bounds() {
118        let mut pm = Pixmap::new(PhysicalSize::new(2, 2));
119        pm.as_bytes_mut()[4..8].copy_from_slice(&[10, 20, 30, 40]);
120        assert_eq!(pm.pixel(1, 0), Some([10, 20, 30, 40]));
121        assert_eq!(pm.pixel(2, 0), None);
122        assert_eq!(pm.stride(), 8);
123    }
124
125    #[test]
126    fn blit_composites_at_offset() {
127        let mut dst = Pixmap::new(PhysicalSize::new(4, 4));
128        let mut src = Pixmap::new(PhysicalSize::new(2, 2));
129        for px in src.as_bytes_mut().chunks_exact_mut(4) {
130            px.copy_from_slice(&[1, 2, 3, 4]);
131        }
132        dst.blit(&src, 1, 1);
133        // The 2x2 block at (1,1) is filled; (0,0) stays transparent.
134        assert_eq!(dst.pixel(0, 0), Some([0, 0, 0, 0]));
135        assert_eq!(dst.pixel(1, 1), Some([1, 2, 3, 4]));
136        assert_eq!(dst.pixel(2, 2), Some([1, 2, 3, 4]));
137        assert_eq!(dst.pixel(3, 3), Some([0, 0, 0, 0]));
138    }
139
140    #[test]
141    fn blit_clips_to_destination_bounds() {
142        let mut dst = Pixmap::new(PhysicalSize::new(2, 2));
143        let mut src = Pixmap::new(PhysicalSize::new(2, 2));
144        for px in src.as_bytes_mut().chunks_exact_mut(4) {
145            px.copy_from_slice(&[9, 9, 9, 9]);
146        }
147        // Origin near the far corner: only the (1,1) pixel lands inside.
148        dst.blit(&src, 1, 1);
149        assert_eq!(dst.pixel(1, 1), Some([9, 9, 9, 9]));
150        assert_eq!(dst.pixel(0, 0), Some([0, 0, 0, 0]));
151        // Fully out of bounds: no-op, no panic.
152        dst.blit(&src, 5, 5);
153    }
154}