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;