Skip to main content

smudge_cpu/
renderer.rs

1//! The renderer that keeps a surface between frames and repaints only the
2//! damaged rectangles of each one.
3
4use smudge_core::app::FrameOutput;
5use smudge_core::geometry::{PixelRect, Point};
6use smudge_core::paint::DisplayItem;
7use smudge_core::renderer::Renderer;
8use tiny_skia::Pixmap;
9
10use crate::draw::{Canvas, PlannedOp, RasterPlan, Surface, for_each_op};
11use crate::glyph_cache::GlyphCache;
12
13/// A renderer that keeps a premultiplied RGBA surface between frames and
14/// repaints only the damaged rectangles of each frame.
15///
16/// Every frame leaves the surface equal to a full repaint of the display list.
17/// Each damaged rectangle is cleared and redrawn in place, and every other
18/// pixel is left untouched. A pixel's value depends only on its position and
19/// the operations covering it, so redrawing a rectangle gives the same bytes a
20/// full repaint would.
21///
22/// Glyphs are drawn from a cache of alpha masks that survives between frames
23/// with the surface, so a glyph is rasterized once no matter how often it is
24/// repainted.
25///
26/// A frame allocates nothing once the surface has seen a frame like it: the
27/// per frame plan of drawable operations reuses its buffers, and a glyph seen
28/// before comes from the cache.
29///
30/// # Examples
31///
32/// ```
33/// use smudge_core::app::FrameOutput;
34/// use smudge_core::geometry::{Color, PixelRect};
35/// use smudge_core::paint::DisplayList;
36/// use smudge_cpu::CpuRenderer;
37///
38/// let mut renderer = CpuRenderer::new(4, 4);
39/// let list = DisplayList::new();
40/// let damage = [PixelRect {
41///     x: 0,
42///     y: 0,
43///     width: 4,
44///     height: 4,
45/// }];
46///
47/// renderer.render(&FrameOutput {
48///     list: &list,
49///     damage: &damage,
50///     full: true,
51///     clear_color: Color::WHITE,
52///     scale: 1.0,
53/// });
54///
55/// assert_eq!(renderer.pixels().len(), 4 * 4 * 4);
56/// assert_eq!(&renderer.pixels()[..4], &[255, 255, 255, 255]);
57/// ```
58pub struct CpuRenderer {
59    /// The surface that survives between frames, or `None` when the surface
60    /// has no pixel.
61    pixmap: Option<Pixmap>,
62    /// The glyph masks that survive between frames.
63    glyphs: GlyphCache,
64    /// The display list's drawable operations of the frame being repainted,
65    /// with their buffers reused between frames.
66    plan: RasterPlan,
67}
68
69impl CpuRenderer {
70    /// Creates a renderer for a surface of `width` by `height` physical pixels.
71    ///
72    /// The surface starts empty. A surface with a zero extent holds no pixels,
73    /// and [`CpuRenderer::width`], [`CpuRenderer::height`],
74    /// [`CpuRenderer::pixels`] and [`CpuRenderer::encode_png`] then report an
75    /// empty surface.
76    pub fn new(width: u32, height: u32) -> CpuRenderer {
77        CpuRenderer {
78            pixmap: Pixmap::new(width, height),
79            glyphs: GlyphCache::new(),
80            plan: RasterPlan::new(),
81        }
82    }
83
84    /// Replaces the surface with one of `width` by `height` physical pixels.
85    ///
86    /// The contents are undefined until the next frame that damages the whole
87    /// surface, so callers resize only when they also force a full repaint.
88    /// The glyph cache survives a resize, because a mask depends on the glyph
89    /// and its size but not on the surface.
90    pub fn resize(&mut self, width: u32, height: u32) {
91        self.pixmap = Pixmap::new(width, height);
92    }
93
94    /// Repaints the damaged rectangles of `frame` into the surface.
95    ///
96    /// Each rectangle of [`FrameOutput::damage`] is rendered separately, so the
97    /// pixels outside every rectangle keep the value the previous frame left
98    /// them with. The damage already covers everything that changed, so
99    /// rendering it produces the same surface a full repaint of the frame would.
100    ///
101    /// Only the damage is read. [`FrameOutput::full`] does not clear the
102    /// surface by itself, and a full frame from the app already damages the
103    /// whole surface.
104    ///
105    /// # Panics
106    ///
107    /// Panics when a damaged rectangle reaches outside the renderer's surface,
108    /// which means the frame was produced for a surface of a different size.
109    /// That is a broken invariant of the caller, not input from outside the
110    /// library.
111    pub fn render(&mut self, frame: &FrameOutput<'_>) {
112        let width = self.width();
113        let height = self.height();
114        for rect in frame.damage {
115            assert!(
116                inside_surface(*rect, width, height),
117                "damage {rect:?} reaches outside the {width}x{height} surface, \
118                 so the frame was produced for a different surface size"
119            );
120        }
121        let Some(pixmap) = self.pixmap.as_mut() else {
122            return;
123        };
124        if frame.damage.is_empty() {
125            return;
126        }
127        let mut canvas = Canvas {
128            pixels: pixmap.data_mut(),
129            width,
130            scale: frame.scale,
131            glyphs: &mut self.glyphs,
132        };
133        match frame.damage {
134            [rect] => {
135                // One rectangle repaints in a single walk, without collecting
136                // the operations first.
137                paint_single(
138                    &mut canvas,
139                    &mut self.plan.clips,
140                    &mut self.plan.offsets,
141                    frame,
142                    *rect,
143                    height,
144                );
145            }
146            damage => {
147                self.plan
148                    .build(frame.list.items(), frame.scale, width, height);
149                for &rect in damage {
150                    paint_rect(&mut canvas, frame, self.plan.ops(), rect);
151                }
152            }
153        }
154    }
155
156    /// Returns the surface width in physical pixels.
157    pub fn width(&self) -> u32 {
158        self.pixmap.as_ref().map_or(0, Pixmap::width)
159    }
160
161    /// Returns the surface height in physical pixels.
162    pub fn height(&self) -> u32 {
163        self.pixmap.as_ref().map_or(0, Pixmap::height)
164    }
165
166    /// Returns the surface as premultiplied RGBA8 pixels, row-major with no
167    /// padding between rows.
168    ///
169    /// The slice is empty when the surface has no pixel.
170    pub fn pixels(&self) -> &[u8] {
171        self.pixmap.as_ref().map_or(&[], Pixmap::data)
172    }
173
174    /// Encodes the surface as a PNG image.
175    ///
176    /// The result is empty when the surface has no pixel, because an image
177    /// cannot have a zero extent.
178    ///
179    /// # Panics
180    ///
181    /// Panics when the PNG encoder fails. Encoding into memory cannot fail, so
182    /// this reports a broken invariant of the encoder rather than anything a
183    /// caller can provoke.
184    pub fn encode_png(&self) -> Vec<u8> {
185        let Some(pixmap) = &self.pixmap else {
186            return Vec::new();
187        };
188        pixmap
189            .encode_png()
190            .expect("encoding a surface into memory cannot fail")
191    }
192}
193
194/// Returns `true` when every pixel of `rect` lies inside a surface of `width`
195/// by `height` pixels.
196fn inside_surface(rect: PixelRect, width: u32, height: u32) -> bool {
197    let right = u64::from(rect.x) + u64::from(rect.width);
198    let bottom = u64::from(rect.y) + u64::from(rect.height);
199    right <= u64::from(width) && bottom <= u64::from(height)
200}
201
202/// Clears `rect` and repaints every planned operation that reaches into it.
203fn paint_rect(
204    canvas: &mut Canvas<'_>,
205    frame: &FrameOutput<'_>,
206    plan: &[PlannedOp],
207    rect: PixelRect,
208) {
209    canvas.clear(rect, frame.clear_color);
210    let items = frame.list.items();
211    for planned in plan {
212        if let Some(part) = planned.area.intersect(rect) {
213            let DisplayItem::Op(op) = &items[planned.index] else {
214                unreachable!("a planned index is an operation item");
215            };
216            canvas.draw(op, part, planned.offset);
217        }
218    }
219}
220
221/// Clears `rect` and repaints every operation that reaches into it, walking
222/// the display list once with `rect` as the clip it repaints.
223fn paint_single(
224    canvas: &mut Canvas<'_>,
225    clips: &mut Vec<Option<PixelRect>>,
226    offsets: &mut Vec<Point>,
227    frame: &FrameOutput<'_>,
228    rect: PixelRect,
229    height: u32,
230) {
231    canvas.clear(rect, frame.clear_color);
232    let width = canvas.width;
233    let scale = frame.scale;
234    let items = frame.list.items();
235    for_each_op(
236        |_, op, area, offset| canvas.draw(op, area, offset),
237        clips,
238        offsets,
239        items,
240        Surface {
241            scale,
242            width,
243            height,
244        },
245        rect,
246    );
247}
248
249impl Renderer for CpuRenderer {
250    fn render(&mut self, frame: &FrameOutput<'_>) {
251        CpuRenderer::render(self, frame);
252    }
253}
254
255#[cfg(test)]
256#[path = "renderer_tests.rs"]
257mod tests;