Skip to main content

denise_render/
coverage.rs

1//! Compositing 8-bit coverage masks.
2//!
3//! An anti-aliased glyph is a rectangle of coverage values, and drawing it is the
4//! single most repeated operation a text-heavy panel performs. The M1 benches
5//! measured the per-pixel path at **31 Mpx/s against 457 Mpx/s for the span
6//! path** on a Pi 3 — fifteen times slower — and predicted that glyphs would be
7//! where that gap gets paid. This is the code that stops it being paid.
8//!
9//! The trick is that a mask is mostly not partial. The interior of a glyph is
10//! solid 255 and everything outside it is 0; only the rim is in between. So the
11//! blitter walks each row in runs, sends solid runs through the span blend, skips
12//! empty runs entirely, and pays the per-pixel cost only on the edge.
13
14use denise::{Point, Rect};
15
16use crate::blend::{Paint, blend_span};
17use crate::canvas::Canvas;
18
19/// A borrowed 8-bit coverage mask: `0` transparent, `255` fully covered.
20#[derive(Clone, Copy, Debug)]
21pub struct Mask<'a> {
22    data: &'a [u8],
23    width: i32,
24    height: i32,
25    stride: usize,
26}
27
28impl<'a> Mask<'a> {
29    /// Wraps a coverage buffer. Returns `None` if it is too small for the
30    /// geometry, or if `stride` is narrower than `width`.
31    pub fn new(data: &'a [u8], width: i32, height: i32, stride: usize) -> Option<Self> {
32        if width <= 0 || height <= 0 || stride < width as usize {
33            return None;
34        }
35        let required = stride * (height as usize - 1) + width as usize;
36        (data.len() >= required).then_some(Self {
37            data,
38            width,
39            height,
40            stride,
41        })
42    }
43
44    /// A mask whose rows are contiguous.
45    pub fn packed(data: &'a [u8], width: i32, height: i32) -> Option<Self> {
46        Self::new(data, width, height, width.max(0) as usize)
47    }
48
49    /// Width in pixels.
50    #[inline]
51    pub const fn width(&self) -> i32 {
52        self.width
53    }
54
55    /// Height in pixels.
56    #[inline]
57    pub const fn height(&self) -> i32 {
58        self.height
59    }
60
61    /// Extent as a rectangle placed at `at`.
62    #[inline]
63    pub const fn bounds_at(&self, at: Point) -> Rect {
64        Rect::new(at.x, at.y, self.width, self.height)
65    }
66
67    #[inline]
68    fn row(&self, y: i32) -> &'a [u8] {
69        let base = y as usize * self.stride;
70        &self.data[base..base + self.width as usize]
71    }
72}
73
74impl Canvas<'_> {
75    /// Composites `mask` in `color`, with its top-left corner at `at`.
76    ///
77    /// Coverage multiplies the paint's own alpha, so a half-transparent colour
78    /// through a half-covered pixel lands at a quarter, which is what it should be.
79    pub fn blit_mask(&mut self, at: Point, mask: &Mask<'_>, color: impl Into<Paint>) {
80        let bounds = mask.bounds_at(at);
81        let Some(visible) = self.visible(bounds) else {
82            return;
83        };
84        let paint = color.into();
85        if paint.alpha() == 0 {
86            return;
87        }
88        let opaque = paint.alpha() == 255;
89
90        for y in visible.y..visible.bottom() {
91            let row = mask.row(y - at.y);
92            let first = (visible.x - at.x) as usize;
93            let last = (visible.right() - at.x) as usize;
94            let mut x = first;
95            while x < last {
96                let value = row[x];
97                if value == 0 {
98                    x += 1;
99                    continue;
100                }
101                // A run of identical coverage. Solid runs — the inside of the
102                // glyph — go through the span blend; everything else is the rim,
103                // which is narrow by construction.
104                let mut end = x + 1;
105                while end < last && row[end] == value {
106                    end += 1;
107                }
108                let start_x = at.x + x as i32;
109                let end_x = at.x + end as i32;
110                if value == 255 && opaque {
111                    if let Some(span) = self.row_span(y, start_x, end_x) {
112                        blend_span(span, paint);
113                    }
114                } else {
115                    let coverage = u32::from(value);
116                    for px in start_x..end_x {
117                        self.blend_at(px, y, paint, coverage);
118                    }
119                }
120                x = end;
121            }
122        }
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use crate::testing::TestCanvas;
130    use denise::Color;
131
132    /// A mask with a solid interior and a half-covered rim, like a real glyph.
133    fn ring() -> ([u8; 36], i32, i32) {
134        let mut data = [0u8; 36];
135        for y in 0..6 {
136            for x in 0..6 {
137                let edge = x == 0 || y == 0 || x == 5 || y == 5;
138                data[y * 6 + x] = if edge { 128 } else { 255 };
139            }
140        }
141        (data, 6, 6)
142    }
143
144    #[test]
145    fn geometry_is_validated() {
146        let data = [0u8; 10];
147        assert!(
148            Mask::packed(&data, 4, 4).is_none(),
149            "16 bytes needed, 10 given"
150        );
151        assert!(Mask::new(&data, 4, 2, 2).is_none(), "stride below width");
152        assert!(Mask::packed(&data, 0, 4).is_none());
153        assert!(Mask::packed(&data, 5, 2).is_some());
154    }
155
156    #[test]
157    fn solid_coverage_paints_the_colour_exactly() {
158        let data = [255u8; 16];
159        let mask = Mask::packed(&data, 4, 4).expect("mask");
160        let mut t = TestCanvas::new(8, 8);
161        t.canvas().blit_mask(Point::new(2, 2), &mask, Color::WHITE);
162        for y in 0..8usize {
163            for x in 0..8usize {
164                let inside = (2..6).contains(&x) && (2..6).contains(&y);
165                let expected = if inside { 0xFFFF_FFFF } else { 0 };
166                assert_eq!(t.pixels()[y * 8 + x], expected, "at {x},{y}");
167            }
168        }
169    }
170
171    #[test]
172    fn partial_coverage_lands_between_the_endpoints() {
173        let data = [128u8; 4];
174        let mask = Mask::packed(&data, 2, 2).expect("mask");
175        let mut t = TestCanvas::new(4, 4);
176        t.canvas().blit_mask(Point::ZERO, &mask, Color::WHITE);
177        let px = t.pixels()[0] & 0xFF;
178        assert!(
179            (120..=136).contains(&px),
180            "half coverage over black should be about half white, got {px}"
181        );
182    }
183
184    #[test]
185    fn coverage_multiplies_the_paint_alpha() {
186        let data = [128u8; 4];
187        let mask = Mask::packed(&data, 2, 2).expect("mask");
188        let mut t = TestCanvas::new(4, 4);
189        t.canvas()
190            .blit_mask(Point::ZERO, &mask, Color::rgba(255, 255, 255, 128));
191        let px = t.pixels()[0] & 0xFF;
192        assert!(
193            (56..=72).contains(&px),
194            "half alpha through half coverage should be about a quarter, got {px}"
195        );
196    }
197
198    #[test]
199    fn the_span_path_and_the_pixel_path_agree() {
200        // The optimisation only holds if a solid run blitted as a span is
201        // identical to the same run blitted pixel by pixel. Draw the same ring
202        // twice, once opaque (span path) and once at alpha 254 (pixel path), and
203        // require the interiors to differ by at most rounding.
204        let (data, w, h) = ring();
205        let mask = Mask::packed(&data, w, h).expect("mask");
206
207        let mut span = TestCanvas::new(8, 8);
208        span.canvas().blit_mask(Point::ZERO, &mask, Color::WHITE);
209        let mut pixel = TestCanvas::new(8, 8);
210        pixel
211            .canvas()
212            .blit_mask(Point::ZERO, &mask, Color::rgba(255, 255, 255, 254));
213
214        for i in 0..64 {
215            let a = span.pixels()[i] & 0xFF;
216            let b = pixel.pixels()[i] & 0xFF;
217            assert!(a.abs_diff(b) <= 2, "paths disagree at {i}: {a} vs {b}");
218        }
219    }
220
221    #[test]
222    fn zero_coverage_writes_nothing() {
223        let data = [0u8; 16];
224        let mask = Mask::packed(&data, 4, 4).expect("mask");
225        let mut t = TestCanvas::new(8, 8);
226        t.canvas().blit_mask(Point::ZERO, &mask, Color::WHITE);
227        assert!(t.pixels().iter().all(|&p| p == 0));
228    }
229
230    #[test]
231    fn a_transparent_paint_writes_nothing() {
232        let data = [255u8; 16];
233        let mask = Mask::packed(&data, 4, 4).expect("mask");
234        let mut t = TestCanvas::new(8, 8);
235        t.canvas()
236            .blit_mask(Point::ZERO, &mask, Color::rgba(255, 0, 0, 0));
237        assert!(t.pixels().iter().all(|&p| p == 0));
238    }
239
240    #[test]
241    fn a_mask_is_clipped_like_everything_else() {
242        let (data, w, h) = ring();
243        let mask = Mask::packed(&data, w, h).expect("mask");
244        let mut t = TestCanvas::new(16, 16);
245        {
246            let mut c = t.canvas();
247            let mut clipped = c.with_clip(Rect::new(4, 4, 4, 4));
248            // Straddles the clip on every side.
249            clipped.blit_mask(Point::new(2, 2), &mask, Color::WHITE);
250        }
251        for y in 0..16usize {
252            for x in 0..16usize {
253                let inside = (4..8).contains(&x) && (4..8).contains(&y);
254                if !inside {
255                    assert_eq!(t.pixels()[y * 16 + x], 0, "drew past the clip at {x},{y}");
256                }
257            }
258        }
259        assert_ne!(t.pixels()[4 * 16 + 4], 0, "nothing was drawn at all");
260    }
261
262    #[test]
263    fn a_mask_partly_off_the_top_left_draws_its_visible_part() {
264        // Negative placement is what happens to a glyph with a left bearing at the
265        // start of a clipped line, and indexing the mask by the clipped coordinate
266        // rather than the placed one is the bug it produces.
267        let data = [255u8; 16];
268        let mask = Mask::packed(&data, 4, 4).expect("mask");
269        let mut t = TestCanvas::new(8, 8);
270        t.canvas()
271            .blit_mask(Point::new(-2, -2), &mask, Color::WHITE);
272        assert_eq!(t.pixels()[0], 0xFFFF_FFFF);
273        assert_eq!(t.pixels()[8 + 1], 0xFFFF_FFFF);
274        assert_eq!(t.pixels()[2 * 8 + 2], 0, "the mask is only 4 wide");
275    }
276
277    #[test]
278    fn a_padded_stride_reads_the_right_rows() {
279        // Atlas rows are slices of a wider buffer, so the stride is almost never
280        // the glyph's width.
281        let mut data = [7u8; 4 * 10];
282        for y in 0..4 {
283            for x in 0..3 {
284                data[y * 10 + x] = 255;
285            }
286        }
287        let mask = Mask::new(&data, 3, 4, 10).expect("mask");
288        let mut t = TestCanvas::new(8, 8);
289        t.canvas().blit_mask(Point::ZERO, &mask, Color::WHITE);
290        for y in 0..4usize {
291            for x in 0..8usize {
292                let expected = if x < 3 { 0xFFFF_FFFF } else { 0 };
293                assert_eq!(t.pixels()[y * 8 + x], expected, "at {x},{y}");
294            }
295        }
296    }
297}