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
14pub use denise::Mask;
15
16use denise::Point;
17
18use crate::blend::{Paint, blend_span};
19use crate::canvas::Canvas;
20
21impl Canvas<'_> {
22    /// Composites `mask` in `color`, with its top-left corner at `at`.
23    ///
24    /// Coverage multiplies the paint's own alpha, so a half-transparent colour
25    /// through a half-covered pixel lands at a quarter, which is what it should be.
26    pub fn blit_mask(&mut self, at: Point, mask: &Mask<'_>, color: impl Into<Paint>) {
27        let bounds = mask.bounds_at(at);
28        let Some(visible) = self.visible(bounds) else {
29            return;
30        };
31        let paint = color.into();
32        if paint.alpha() == 0 {
33            return;
34        }
35        let opaque = paint.alpha() == 255;
36
37        for y in visible.y..visible.bottom() {
38            let row = mask.row(y - at.y);
39            let first = (visible.x - at.x) as usize;
40            let last = (visible.right() - at.x) as usize;
41            let mut x = first;
42            while x < last {
43                let value = row[x];
44                if value == 0 {
45                    x += 1;
46                    continue;
47                }
48                // A run of identical coverage. Solid runs — the inside of the
49                // glyph — go through the span blend; everything else is the rim,
50                // which is narrow by construction.
51                let mut end = x + 1;
52                while end < last && row[end] == value {
53                    end += 1;
54                }
55                let start_x = at.x + x as i32;
56                let end_x = at.x + end as i32;
57                if value == 255 && opaque {
58                    if let Some(span) = self.row_span(y, start_x, end_x) {
59                        blend_span(span, paint);
60                    }
61                } else {
62                    let coverage = u32::from(value);
63                    for px in start_x..end_x {
64                        self.blend_at(px, y, paint, coverage);
65                    }
66                }
67                x = end;
68            }
69        }
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76    use crate::testing::TestCanvas;
77    use denise::Color;
78    use denise::Rect;
79
80    /// A mask with a solid interior and a half-covered rim, like a real glyph.
81    fn ring() -> ([u8; 36], i32, i32) {
82        let mut data = [0u8; 36];
83        for y in 0..6 {
84            for x in 0..6 {
85                let edge = x == 0 || y == 0 || x == 5 || y == 5;
86                data[y * 6 + x] = if edge { 128 } else { 255 };
87            }
88        }
89        (data, 6, 6)
90    }
91
92    #[test]
93    fn geometry_is_validated() {
94        let data = [0u8; 10];
95        assert!(
96            Mask::packed(&data, 4, 4).is_none(),
97            "16 bytes needed, 10 given"
98        );
99        assert!(Mask::new(&data, 4, 2, 2).is_none(), "stride below width");
100        assert!(Mask::packed(&data, 0, 4).is_none());
101        assert!(Mask::packed(&data, 5, 2).is_some());
102    }
103
104    #[test]
105    fn solid_coverage_paints_the_colour_exactly() {
106        let data = [255u8; 16];
107        let mask = Mask::packed(&data, 4, 4).expect("mask");
108        let mut t = TestCanvas::new(8, 8);
109        t.canvas().blit_mask(Point::new(2, 2), &mask, Color::WHITE);
110        for y in 0..8usize {
111            for x in 0..8usize {
112                let inside = (2..6).contains(&x) && (2..6).contains(&y);
113                let expected = if inside { 0xFFFF_FFFF } else { 0 };
114                assert_eq!(t.pixels()[y * 8 + x], expected, "at {x},{y}");
115            }
116        }
117    }
118
119    #[test]
120    fn partial_coverage_lands_between_the_endpoints() {
121        let data = [128u8; 4];
122        let mask = Mask::packed(&data, 2, 2).expect("mask");
123        let mut t = TestCanvas::new(4, 4);
124        t.canvas().blit_mask(Point::ZERO, &mask, Color::WHITE);
125        let px = t.pixels()[0] & 0xFF;
126        assert!(
127            (120..=136).contains(&px),
128            "half coverage over black should be about half white, got {px}"
129        );
130    }
131
132    #[test]
133    fn coverage_multiplies_the_paint_alpha() {
134        let data = [128u8; 4];
135        let mask = Mask::packed(&data, 2, 2).expect("mask");
136        let mut t = TestCanvas::new(4, 4);
137        t.canvas()
138            .blit_mask(Point::ZERO, &mask, Color::rgba(255, 255, 255, 128));
139        let px = t.pixels()[0] & 0xFF;
140        assert!(
141            (56..=72).contains(&px),
142            "half alpha through half coverage should be about a quarter, got {px}"
143        );
144    }
145
146    #[test]
147    fn the_span_path_and_the_pixel_path_agree() {
148        // The optimisation only holds if a solid run blitted as a span is
149        // identical to the same run blitted pixel by pixel. Draw the same ring
150        // twice, once opaque (span path) and once at alpha 254 (pixel path), and
151        // require the interiors to differ by at most rounding.
152        let (data, w, h) = ring();
153        let mask = Mask::packed(&data, w, h).expect("mask");
154
155        let mut span = TestCanvas::new(8, 8);
156        span.canvas().blit_mask(Point::ZERO, &mask, Color::WHITE);
157        let mut pixel = TestCanvas::new(8, 8);
158        pixel
159            .canvas()
160            .blit_mask(Point::ZERO, &mask, Color::rgba(255, 255, 255, 254));
161
162        for i in 0..64 {
163            let a = span.pixels()[i] & 0xFF;
164            let b = pixel.pixels()[i] & 0xFF;
165            assert!(a.abs_diff(b) <= 2, "paths disagree at {i}: {a} vs {b}");
166        }
167    }
168
169    #[test]
170    fn zero_coverage_writes_nothing() {
171        let data = [0u8; 16];
172        let mask = Mask::packed(&data, 4, 4).expect("mask");
173        let mut t = TestCanvas::new(8, 8);
174        t.canvas().blit_mask(Point::ZERO, &mask, Color::WHITE);
175        assert!(t.pixels().iter().all(|&p| p == 0));
176    }
177
178    #[test]
179    fn a_transparent_paint_writes_nothing() {
180        let data = [255u8; 16];
181        let mask = Mask::packed(&data, 4, 4).expect("mask");
182        let mut t = TestCanvas::new(8, 8);
183        t.canvas()
184            .blit_mask(Point::ZERO, &mask, Color::rgba(255, 0, 0, 0));
185        assert!(t.pixels().iter().all(|&p| p == 0));
186    }
187
188    #[test]
189    fn a_mask_is_clipped_like_everything_else() {
190        let (data, w, h) = ring();
191        let mask = Mask::packed(&data, w, h).expect("mask");
192        let mut t = TestCanvas::new(16, 16);
193        {
194            let mut c = t.canvas();
195            let mut clipped = c.with_clip(Rect::new(4, 4, 4, 4));
196            // Straddles the clip on every side.
197            clipped.blit_mask(Point::new(2, 2), &mask, Color::WHITE);
198        }
199        for y in 0..16usize {
200            for x in 0..16usize {
201                let inside = (4..8).contains(&x) && (4..8).contains(&y);
202                if !inside {
203                    assert_eq!(t.pixels()[y * 16 + x], 0, "drew past the clip at {x},{y}");
204                }
205            }
206        }
207        assert_ne!(t.pixels()[4 * 16 + 4], 0, "nothing was drawn at all");
208    }
209
210    #[test]
211    fn a_mask_partly_off_the_top_left_draws_its_visible_part() {
212        // Negative placement is what happens to a glyph with a left bearing at the
213        // start of a clipped line, and indexing the mask by the clipped coordinate
214        // rather than the placed one is the bug it produces.
215        let data = [255u8; 16];
216        let mask = Mask::packed(&data, 4, 4).expect("mask");
217        let mut t = TestCanvas::new(8, 8);
218        t.canvas()
219            .blit_mask(Point::new(-2, -2), &mask, Color::WHITE);
220        assert_eq!(t.pixels()[0], 0xFFFF_FFFF);
221        assert_eq!(t.pixels()[8 + 1], 0xFFFF_FFFF);
222        assert_eq!(t.pixels()[2 * 8 + 2], 0, "the mask is only 4 wide");
223    }
224
225    #[test]
226    fn a_padded_stride_reads_the_right_rows() {
227        // Atlas rows are slices of a wider buffer, so the stride is almost never
228        // the glyph's width.
229        let mut data = [7u8; 4 * 10];
230        for y in 0..4 {
231            for x in 0..3 {
232                data[y * 10 + x] = 255;
233            }
234        }
235        let mask = Mask::new(&data, 3, 4, 10).expect("mask");
236        let mut t = TestCanvas::new(8, 8);
237        t.canvas().blit_mask(Point::ZERO, &mask, Color::WHITE);
238        for y in 0..4usize {
239            for x in 0..8usize {
240                let expected = if x < 3 { 0xFFFF_FFFF } else { 0 };
241                assert_eq!(t.pixels()[y * 8 + x], expected, "at {x},{y}");
242            }
243        }
244    }
245}