Skip to main content

alpha2_geometry/raster/
mod.rs

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