Skip to main content

alpha2_geometry/raster/
mod.rs

1//! Coverage of a path: per pixel, the average winding number over the
2//! pixel's area, mapped by the fill rule, in 16 by 16 pixel tiles.
3//!
4//! Lines are accumulated only in the columns an edge touches, one strip of
5//! tile rows at a time ([`rasterize`]); a tile no edge reaches takes the
6//! winding of the run it lies in, without per-pixel work, and is [`Full`]
7//! or [`Empty`]. The prefix sums add the same non-zero contributions in the
8//! same order as accumulating every pixel would ([`dense::rasterize_dense`],
9//! kept as the reference), so both give the same bytes.
10//!
11//! Everything is scalar code in a fixed order: `f64` geometry, `f32`
12//! accumulation, no fused multiply-add. Coverage is the same bytes on every
13//! target.
14//!
15//! [`Full`]: TileState::Full
16//! [`Empty`]: TileState::Empty
17
18mod accumulate;
19mod cache;
20pub mod dense;
21mod flatten;
22
23pub use cache::{CoverageCache, coverage_key};
24
25use crate::IRect;
26use accumulate::{accumulate_line, clip_line};
27use alpha2_model::FillRule;
28use kurbo::{BezPath, Point};
29
30/// Tile edge, in pixels.
31pub const TILE: usize = 16;
32
33/// Bytes of one tile's coverage.
34pub const TILE_BYTES: usize = TILE * TILE;
35
36/// What one tile of coverage holds.
37#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
38pub enum TileState {
39    /// No coverage anywhere in it.
40    Empty,
41    /// Full coverage everywhere in it.
42    Full,
43    /// Coverage that varies: the index of its bytes in
44    /// [`TileCoverage::partial`].
45    Partial(u32),
46}
47
48/// A path's coverage over a rectangle of whole tiles.
49#[derive(Clone, Debug, Default, Eq, PartialEq)]
50pub struct TileCoverage {
51    /// The first tile's position, in tiles: pixel `(16 x, 16 y)`.
52    pub origin: (i32, i32),
53    /// Tiles across and down.
54    pub size: (u32, u32),
55    /// Each tile, row by row.
56    pub tiles: Vec<TileState>,
57    /// The bytes of each partial tile, row by row within it.
58    pub partial: Vec<[u8; TILE_BYTES]>,
59}
60
61impl TileCoverage {
62    /// The coverage of pixel `(x, y)`, zero outside the tiles.
63    #[must_use]
64    pub fn at(&self, x: i32, y: i32) -> u8 {
65        let tile = i32::try_from(TILE).unwrap_or(16);
66        let (tx, ty) = (
67            x.div_euclid(tile) - self.origin.0,
68            y.div_euclid(tile) - self.origin.1,
69        );
70        let (Ok(tx), Ok(ty)) = (u32::try_from(tx), u32::try_from(ty)) else {
71            return 0;
72        };
73        if tx >= self.size.0 || ty >= self.size.1 {
74            return 0;
75        }
76        match self.tiles[(ty * self.size.0 + tx) as usize] {
77            TileState::Empty => 0,
78            TileState::Full => 255,
79            TileState::Partial(index) => {
80                let (cx, cy) = (x.rem_euclid(tile), y.rem_euclid(tile));
81                self.partial[index as usize][usize::try_from(cy * tile + cx).unwrap_or(0)]
82            }
83        }
84    }
85
86    /// The pixel rectangle the tiles cover.
87    #[must_use]
88    pub fn rect(&self) -> IRect {
89        let tile = i32::try_from(TILE).unwrap_or(16);
90        let width = i32::try_from(self.size.0).unwrap_or(0);
91        let height = i32::try_from(self.size.1).unwrap_or(0);
92        IRect::new(
93            self.origin.0 * tile,
94            self.origin.1 * tile,
95            (self.origin.0 + width) * tile,
96            (self.origin.1 + height) * tile,
97        )
98    }
99
100    /// The heap it holds, as a cache counts it.
101    #[must_use]
102    pub fn heap_bytes(&self) -> usize {
103        self.tiles.len() * size_of::<TileState>() + self.partial.len() * TILE_BYTES
104    }
105
106    /// Counts of empty, full, and partial tiles.
107    #[must_use]
108    pub fn counts(&self) -> (usize, usize, usize) {
109        let mut counts = (0, 0, 0);
110        for tile in &self.tiles {
111            match tile {
112                TileState::Empty => counts.0 += 1,
113                TileState::Full => counts.1 += 1,
114                TileState::Partial(_) => counts.2 += 1,
115            }
116        }
117        counts
118    }
119}
120
121/// `clip` grown outward to whole tiles.
122#[must_use]
123pub fn tile_aligned(clip: IRect) -> IRect {
124    let tile = i32::try_from(TILE).unwrap_or(16);
125    if clip.is_empty() {
126        return IRect::default();
127    }
128    IRect::new(
129        clip.x0.div_euclid(tile) * tile,
130        clip.y0.div_euclid(tile) * tile,
131        (clip.x1 + tile - 1).div_euclid(tile) * tile,
132        (clip.y1 + tile - 1).div_euclid(tile) * tile,
133    )
134}
135
136/// The coverage byte of a pixel whose average winding is `winding`.
137/// Even-odd is a triangle wave of period 2, found with a truncating
138/// conversion rather than `floor` (a library call on baseline x86-64).
139#[allow(
140    clippy::cast_possible_truncation,
141    clippy::cast_sign_loss,
142    clippy::cast_precision_loss
143)]
144pub(crate) fn coverage_byte(rule: FillRule, winding: f32) -> u8 {
145    let coverage = match rule {
146        FillRule::Nonzero => winding.abs().min(1.0),
147        FillRule::Evenodd => {
148            let half = winding.abs() * 0.5;
149            let turn = half - (half as i32) as f32;
150            (1.0 - (2.0 * turn - 1.0).abs()).min(1.0)
151        }
152    };
153    (coverage * 255.0 + 0.5) as u8
154}
155
156/// A run of one pixel row: constant between the cells edges touched, per
157/// pixel within them.
158#[derive(Clone, Copy)]
159enum Run {
160    Const { x0: usize, x1: usize, value: u8 },
161    Pixels { x0: usize, x1: usize, offset: usize },
162}
163
164impl Run {
165    const fn x1(self) -> usize {
166        match self {
167            Self::Const { x1, .. } | Self::Pixels { x1, .. } => x1,
168        }
169    }
170}
171
172/// Buffers reused from one rasterization to the next.
173#[derive(Debug, Default)]
174pub struct RasterScratch {
175    lines: Vec<[f64; 4]>,
176    bins: Vec<Vec<u32>>,
177    acc: Vec<f32>,
178    touched: Vec<Vec<(usize, usize)>>,
179    runs: Vec<Vec<RunSlot>>,
180    pixels: Vec<u8>,
181    cursor: Vec<usize>,
182}
183
184/// A [`Run`] as the scratch stores it: kind (0 constant, 1 pixels), `x0`,
185/// `x1`, and the value or offset.
186#[derive(Clone, Copy, Debug)]
187struct RunSlot(u8, usize, usize, usize);
188
189impl From<Run> for RunSlot {
190    fn from(run: Run) -> Self {
191        match run {
192            Run::Const { x0, x1, value } => Self(0, x0, x1, usize::from(value)),
193            Run::Pixels { x0, x1, offset } => Self(1, x0, x1, offset),
194        }
195    }
196}
197
198impl From<RunSlot> for Run {
199    #[allow(clippy::cast_possible_truncation)]
200    fn from(slot: RunSlot) -> Self {
201        if slot.0 == 0 {
202            Self::Const {
203                x0: slot.1,
204                x1: slot.2,
205                value: slot.3 as u8,
206            }
207        } else {
208            Self::Pixels {
209                x0: slot.1,
210                x1: slot.2,
211                offset: slot.3,
212            }
213        }
214    }
215}
216
217/// The coverage of `path` under `rule` within `clip` (device pixels, grown
218/// to whole tiles): per pixel, the average winding number over its area,
219/// mapped by the rule. Nonzero is `min(|w|, 1)`; even-odd a triangle wave
220/// of period 2 in `|w|`. Only pixels where three or more windings cross
221/// within the pixel are approximate.
222#[must_use]
223// One pass over strips, rows, and tiles; split up, its state would cross function boundaries.
224#[allow(clippy::too_many_lines)]
225pub fn rasterize(
226    path: &BezPath,
227    rule: FillRule,
228    clip: IRect,
229    scratch: &mut RasterScratch,
230) -> TileCoverage {
231    let clip = tile_aligned(clip);
232    let tile = i32::try_from(TILE).unwrap_or(16);
233    let (w, h) = (clip.width() as usize, clip.height() as usize);
234    let tiles_x = w / TILE;
235    let tiles_y = h / TILE;
236    let mut out = TileCoverage {
237        origin: (clip.x0.div_euclid(tile), clip.y0.div_euclid(tile)),
238        size: (
239            u32::try_from(tiles_x).unwrap_or(0),
240            u32::try_from(tiles_y).unwrap_or(0),
241        ),
242        tiles: Vec::with_capacity(tiles_x * tiles_y),
243        partial: Vec::new(),
244    };
245    if w == 0 || h == 0 {
246        return out;
247    }
248    flatten::flatten(
249        path,
250        Point::new(f64::from(clip.x0), f64::from(clip.y0)),
251        &mut scratch.lines,
252    );
253    #[allow(clippy::cast_precision_loss)]
254    let (wf, hf) = (w as f64, h as f64);
255    // Lines by the strips (tile rows) they reach.
256    scratch.bins.resize_with(tiles_y, Vec::new);
257    for bin in &mut scratch.bins[..tiles_y] {
258        bin.clear();
259    }
260    for (index, line) in scratch.lines.iter().enumerate() {
261        let (lo, hi) = (line[1].min(line[3]).max(0.0), line[1].max(line[3]).min(hf));
262        if hi <= lo {
263            continue;
264        }
265        // Both within the clip's height here.
266        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
267        let (first, last) = (
268            (lo as usize) / TILE,
269            (hi.ceil() as usize).div_ceil(TILE).min(tiles_y),
270        );
271        let index = u32::try_from(index).unwrap_or(u32::MAX);
272        for bin in &mut scratch.bins[first..last] {
273            bin.push(index);
274        }
275    }
276    let stride = w + 2;
277    scratch.acc.clear();
278    scratch.acc.resize(stride * TILE, 0.0);
279    scratch.touched.resize_with(TILE, Vec::new);
280    scratch.runs.resize_with(TILE, Vec::new);
281    scratch.cursor.resize(TILE, 0);
282    #[allow(clippy::cast_precision_loss)]
283    let wf32 = w as f32;
284    for strip in 0..tiles_y {
285        let r0 = strip * TILE;
286        let r1 = r0 + TILE;
287        for list in &mut scratch.touched[..TILE] {
288            list.clear();
289        }
290        {
291            let RasterScratch {
292                lines,
293                bins,
294                acc,
295                touched,
296                ..
297            } = &mut *scratch;
298            for &index in &bins[strip] {
299                clip_line(lines[index as usize], wf, hf, |p0, p1| {
300                    accumulate_line(acc, stride, r0, (r0, r1), wf32, p0, p1, |y, first, last| {
301                        touched[y - r0].push((first, last));
302                    });
303                });
304            }
305        }
306        // Each row as runs: constant between the touched ranges, per pixel within them.
307        scratch.pixels.clear();
308        for r in 0..TILE {
309            let row = r * stride;
310            let RasterScratch {
311                acc,
312                touched,
313                runs,
314                pixels,
315                ..
316            } = &mut *scratch;
317            let ranges = &mut touched[r];
318            ranges.sort_unstable();
319            let runs = &mut runs[r];
320            runs.clear();
321            let (mut winding, mut cursor) = (0.0f32, 0usize);
322            let mut index = 0;
323            while index < ranges.len() {
324                let (lo, mut hi) = ranges[index];
325                index += 1;
326                while index < ranges.len() && ranges[index].0 <= hi + 1 {
327                    hi = hi.max(ranges[index].1);
328                    index += 1;
329                }
330                if lo > cursor && cursor < w {
331                    runs.push(
332                        Run::Const {
333                            x0: cursor,
334                            x1: lo.min(w),
335                            value: coverage_byte(rule, winding),
336                        }
337                        .into(),
338                    );
339                }
340                let offset = pixels.len();
341                for x in lo..=hi {
342                    winding += acc[row + x];
343                    acc[row + x] = 0.0;
344                    if x < w {
345                        pixels.push(coverage_byte(rule, winding));
346                    }
347                }
348                if lo < w {
349                    runs.push(
350                        Run::Pixels {
351                            x0: lo,
352                            x1: (hi + 1).min(w),
353                            offset,
354                        }
355                        .into(),
356                    );
357                }
358                cursor = hi + 1;
359            }
360            if cursor < w {
361                runs.push(
362                    Run::Const {
363                        x0: cursor,
364                        x1: w,
365                        value: coverage_byte(rule, winding),
366                    }
367                    .into(),
368                );
369            }
370        }
371        // Tiles: uniform when every row lies in one constant run across them.
372        for cursor in &mut scratch.cursor[..TILE] {
373            *cursor = 0;
374        }
375        for tx in 0..tiles_x {
376            let (cx0, cx1) = (tx * TILE, tx * TILE + TILE);
377            let (mut all_full, mut all_empty) = (true, true);
378            for r in 0..TILE {
379                let runs = &scratch.runs[r];
380                let cursor = &mut scratch.cursor[r];
381                while *cursor < runs.len() && Run::from(runs[*cursor]).x1() <= cx0 {
382                    *cursor += 1;
383                }
384                match runs.get(*cursor).map(|slot| Run::from(*slot)) {
385                    Some(Run::Const { x0, x1, value }) if x0 <= cx0 && x1 >= cx1 => {
386                        all_full &= value == 255;
387                        all_empty &= value == 0;
388                    }
389                    _ => {
390                        all_full = false;
391                        all_empty = false;
392                    }
393                }
394            }
395            let state = if all_full {
396                TileState::Full
397            } else if all_empty {
398                TileState::Empty
399            } else {
400                let mut bytes = [0u8; TILE_BYTES];
401                for r in 0..TILE {
402                    let runs = &scratch.runs[r];
403                    let mut at = scratch.cursor[r];
404                    while at < runs.len() {
405                        let run = Run::from(runs[at]);
406                        let (x0, x1) = match run {
407                            Run::Const { x0, x1, .. } | Run::Pixels { x0, x1, .. } => (x0, x1),
408                        };
409                        if x0 >= cx1 {
410                            break;
411                        }
412                        for x in x0.max(cx0)..x1.min(cx1) {
413                            bytes[r * TILE + (x - cx0)] = match run {
414                                Run::Const { value, .. } => value,
415                                Run::Pixels { x0, offset, .. } => scratch.pixels[offset + (x - x0)],
416                            };
417                        }
418                        at += 1;
419                    }
420                }
421                // An edge may pass through a tile and leave it uniform.
422                if bytes.iter().all(|byte| *byte == 255) {
423                    TileState::Full
424                } else if bytes.iter().all(|byte| *byte == 0) {
425                    TileState::Empty
426                } else {
427                    out.partial.push(bytes);
428                    TileState::Partial(u32::try_from(out.partial.len() - 1).unwrap_or(u32::MAX))
429                }
430            };
431            out.tiles.push(state);
432        }
433    }
434    out
435}
436
437/// The flattened lines [`rasterize`] accumulates for `path` in `clip`,
438/// relative to the tile-aligned clip's corner.
439#[must_use]
440pub fn flattened(path: &BezPath, clip: IRect) -> Vec<[f64; 4]> {
441    let clip = tile_aligned(clip);
442    let mut lines = Vec::new();
443    flatten::flatten(
444        path,
445        Point::new(f64::from(clip.x0), f64::from(clip.y0)),
446        &mut lines,
447    );
448    lines
449}
450
451/// The winding number of `path` around `point`, from its flattened lines:
452/// what a fill rule decides inside from, for hit tests.
453#[must_use]
454pub fn winding_at(path: &BezPath, point: Point) -> i32 {
455    let mut lines = Vec::new();
456    flatten::flatten(path, Point::ZERO, &mut lines);
457    let mut winding = 0;
458    for [x0, y0, x1, y1] in lines {
459        // Upward crossings count one way, downward the other; a vertex on the ray counts once.
460        if (y0 <= point.y) != (y1 <= point.y) {
461            let x = x0 + (point.y - y0) * (x1 - x0) / (y1 - y0);
462            if x > point.x {
463                winding += if y1 > y0 { 1 } else { -1 };
464            }
465        }
466    }
467    winding
468}
469
470#[cfg(test)]
471mod tests;