Skip to main content

oxiui_render_soft/
scanline.rs

1//! Active-Edge-Table (AET) scanline rasteriser for polygon and triangle fill.
2//!
3//! Supports sub-pixel vertical-supersample coverage anti-aliasing, even-odd
4//! fill rule, and non-zero winding fill rule. Both are computed by the same
5//! AET machinery — the rule only changes how the accumulated winding count is
6//! interpreted per scanline span.
7
8use crate::framebuffer::Framebuffer;
9use oxiui_core::Color;
10
11/// How self-intersecting or overlapping sub-paths are filled.
12#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
13pub enum FillRule {
14    /// Standard even-odd rule: regions enclosed by an odd number of boundary
15    /// crossings are filled; even crossings produce holes.
16    EvenOdd,
17    /// Non-zero winding rule: a point is inside if the signed crossing count is
18    /// non-zero.
19    #[default]
20    NonZero,
21}
22
23// ---------------------------------------------------------------------------
24// Internal AET machinery
25// ---------------------------------------------------------------------------
26
27/// A single edge for the active-edge-table algorithm, stored in float for
28/// sub-pixel accuracy.
29#[derive(Clone, Debug)]
30pub(crate) struct Edge {
31    /// Current X position at the current scanline's top.
32    x: f32,
33    /// X increment per scanline (dx/dy).
34    dx: f32,
35    /// Scanline at which this edge ends (exclusive).
36    y_max: i32,
37    /// Winding contribution: +1 for upward, -1 for downward (used by
38    /// non-zero winding rule).
39    winding: i32,
40}
41
42// ---------------------------------------------------------------------------
43// Rasterizer scratch buffers — reused across fills to avoid per-polygon alloc
44// ---------------------------------------------------------------------------
45
46/// Scratch buffers owned by a rasterizer, reused across polygon fills.
47///
48/// Call [`RasterizerScratch::clear`] at the start of each fill instead of
49/// creating fresh `Vec`s.
50pub struct RasterizerScratch {
51    /// Global edge table (y_start, Edge) pairs for the current polygon.
52    pub(crate) global_edges: Vec<(i32, Edge)>,
53    /// Active edge table for the current scanline.
54    pub(crate) active: Vec<Edge>,
55}
56
57impl RasterizerScratch {
58    /// Allocate empty scratch buffers.
59    pub fn new() -> Self {
60        Self {
61            global_edges: Vec::new(),
62            active: Vec::new(),
63        }
64    }
65
66    /// Clear both buffers for the next polygon fill (no heap deallocation).
67    pub fn clear(&mut self) {
68        self.global_edges.clear();
69        self.active.clear();
70    }
71}
72
73impl Default for RasterizerScratch {
74    fn default() -> Self {
75        Self::new()
76    }
77}
78
79/// A scanline rasterizer that owns reusable scratch buffers so per-polygon
80/// heap allocation is avoided.
81///
82/// Use [`Rasterizer::fill_polygon`] / [`Rasterizer::fill_polygon_clipped`]
83/// in performance-critical loops. The free-standing functions in this module
84/// delegate here and are kept for API stability.
85pub struct Rasterizer {
86    scratch: RasterizerScratch,
87}
88
89impl Rasterizer {
90    /// Create a new rasterizer with empty scratch buffers.
91    pub fn new() -> Self {
92        Self {
93            scratch: RasterizerScratch::new(),
94        }
95    }
96
97    /// Fill a polygon into `fb`, reusing internal scratch buffers.
98    pub fn fill_polygon(
99        &mut self,
100        fb: &mut Framebuffer,
101        points: &[(f32, f32)],
102        color: Color,
103        fill_rule: FillRule,
104        aa: bool,
105    ) {
106        fill_polygon_with_scratch(fb, points, color, fill_rule, aa, &mut self.scratch);
107    }
108
109    /// Fill a polygon into `fb`, clipped to `clip`, reusing internal scratch.
110    pub fn fill_polygon_clipped(
111        &mut self,
112        fb: &mut Framebuffer,
113        points: &[(f32, f32)],
114        color: Color,
115        fill_rule: FillRule,
116        aa: bool,
117        clip: crate::clip::ClipRect,
118    ) {
119        fill_polygon_clipped_with_scratch(
120            fb,
121            points,
122            color,
123            fill_rule,
124            aa,
125            clip,
126            &mut self.scratch,
127        );
128    }
129}
130
131impl Default for Rasterizer {
132    fn default() -> Self {
133        Self::new()
134    }
135}
136
137/// Build the global edge table from a polygon vertex list, appending into `out`.
138///
139/// Each consecutive pair of vertices forms an edge; the last vertex connects
140/// back to the first to close the polygon. Horizontal edges (dy == 0) are
141/// skipped. `out` is *not* cleared by this function — callers must clear it
142/// before calling if a fresh table is needed.
143fn build_edges_into(points: &[(f32, f32)], out: &mut Vec<(i32, Edge)>) {
144    let n = points.len();
145    if n < 2 {
146        return;
147    }
148    out.reserve(n);
149    for i in 0..n {
150        let (x0, y0) = points[i];
151        let (x1, y1) = points[(i + 1) % n];
152        if (y0 - y1).abs() < f32::EPSILON {
153            // Horizontal — skip.
154            continue;
155        }
156        let (top_y, bot_y, top_x, bot_x, winding) = if y0 < y1 {
157            (y0, y1, x0, x1, 1i32)
158        } else {
159            (y1, y0, x1, x0, -1i32)
160        };
161        let dy = bot_y - top_y;
162        let dx = (bot_x - top_x) / dy;
163        let y_start = top_y.ceil() as i32;
164        // Sub-pixel correction: move x to the y_start scanline centre.
165        let x_at_start = top_x + dx * (y_start as f32 - top_y);
166        let y_max = bot_y.ceil() as i32;
167        out.push((
168            y_start,
169            Edge {
170                x: x_at_start,
171                dx,
172                y_max,
173                winding,
174            },
175        ));
176    }
177}
178
179/// Compute the alpha (coverage) for a single pixel column within a span,
180/// given supersample coverage fraction.
181///
182/// This is a simple closed-form computation: sample 8 sub-rows per pixel
183/// to get fractional coverage at the span boundary. The interior receives 1.0.
184#[inline]
185fn span_coverage(left: f32, right: f32, px: f32) -> f32 {
186    // Pixel occupies [px, px+1).
187    let pl = px;
188    let pr = px + 1.0;
189    if right <= pl || left >= pr {
190        return 0.0;
191    }
192    let cl = left.max(pl);
193    let cr = right.min(pr);
194    (cr - cl).clamp(0.0, 1.0)
195}
196
197/// Core fill implementation that reuses caller-provided scratch buffers.
198fn fill_polygon_with_scratch(
199    fb: &mut Framebuffer,
200    points: &[(f32, f32)],
201    color: Color,
202    fill_rule: FillRule,
203    aa: bool,
204    scratch: &mut RasterizerScratch,
205) {
206    if points.len() < 3 {
207        return;
208    }
209
210    // Bounding box.
211    let (mut min_y, mut max_y) = (f32::INFINITY, f32::NEG_INFINITY);
212    for &(_, y) in points {
213        if y < min_y {
214            min_y = y;
215        }
216        if y > max_y {
217            max_y = y;
218        }
219    }
220    let y_start = min_y.floor() as i32;
221    let y_end = max_y.ceil() as i32;
222
223    // Build global edge table into scratch buffer.
224    scratch.clear();
225    build_edges_into(points, &mut scratch.global_edges);
226
227    // Sort global edge table by y_start ascending, then by x.
228    scratch.global_edges.sort_by(|a, b| {
229        a.0.cmp(&b.0).then(
230            a.1.x
231                .partial_cmp(&b.1.x)
232                .unwrap_or(core::cmp::Ordering::Equal),
233        )
234    });
235
236    let mut gi = 0usize;
237
238    for y in y_start..y_end {
239        // Add edges whose y_start == y.
240        while gi < scratch.global_edges.len() && scratch.global_edges[gi].0 == y {
241            scratch.active.push(scratch.global_edges[gi].1.clone());
242            gi += 1;
243        }
244
245        // Remove expired edges.
246        scratch.active.retain(|e| e.y_max > y);
247
248        // Sort active edges by current x.
249        scratch
250            .active
251            .sort_by(|a, b| a.x.partial_cmp(&b.x).unwrap_or(core::cmp::Ordering::Equal));
252
253        // Fill spans using the chosen fill rule.
254        fill_spans(
255            fb,
256            &scratch.active,
257            y as u32,
258            y as f32,
259            color,
260            fill_rule,
261            aa,
262        );
263
264        // Advance active edges.
265        for e in &mut scratch.active {
266            e.x += e.dx;
267        }
268    }
269}
270
271/// Fill a polygon (closed contour) defined by `points` into `fb`.
272///
273/// `points` is interpreted as an ordered list of (x, y) vertices; the polygon
274/// is implicitly closed (last → first). Uses a vertical-supersample AET for
275/// sub-pixel AA.
276///
277/// Does nothing if there are fewer than 3 points.
278///
279/// For tight loops over many polygons, prefer [`Rasterizer::fill_polygon`]
280/// which reuses scratch buffers across calls.
281pub fn fill_polygon(
282    fb: &mut Framebuffer,
283    points: &[(f32, f32)],
284    color: Color,
285    fill_rule: FillRule,
286    aa: bool,
287) {
288    let mut scratch = RasterizerScratch::new();
289    fill_polygon_with_scratch(fb, points, color, fill_rule, aa, &mut scratch);
290}
291
292/// Process active edges for one scanline using the chosen fill rule.
293fn fill_spans(
294    fb: &mut Framebuffer,
295    active: &[Edge],
296    y: u32,
297    _y_float: f32,
298    color: Color,
299    fill_rule: FillRule,
300    aa: bool,
301) {
302    if y >= fb.height() || active.is_empty() {
303        return;
304    }
305
306    match fill_rule {
307        FillRule::EvenOdd => fill_even_odd(fb, active, y, color, aa),
308        FillRule::NonZero => fill_non_zero(fb, active, y, color, aa),
309    }
310}
311
312/// Even-odd rule: pairs of edges delimit filled spans.
313fn fill_even_odd(fb: &mut Framebuffer, active: &[Edge], y: u32, color: Color, aa: bool) {
314    let mut i = 0;
315    while i + 1 < active.len() {
316        let left = active[i].x;
317        let right = active[i + 1].x;
318        if right > left {
319            paint_span(fb, left, right, y, color, aa);
320        }
321        i += 2;
322    }
323}
324
325/// Non-zero winding rule: accumulate winding; fill while winding != 0.
326fn fill_non_zero(fb: &mut Framebuffer, active: &[Edge], y: u32, color: Color, aa: bool) {
327    let mut winding = 0i32;
328    let mut fill_start: Option<f32> = None;
329
330    for edge in active {
331        let prev_winding = winding;
332        winding += edge.winding;
333
334        if prev_winding == 0 && winding != 0 {
335            // Entering a filled region.
336            fill_start = Some(edge.x);
337        } else if prev_winding != 0 && winding == 0 {
338            // Leaving a filled region.
339            if let Some(start) = fill_start.take() {
340                let end = edge.x;
341                if end > start {
342                    paint_span(fb, start, end, y, color, aa);
343                }
344            }
345        }
346    }
347}
348
349/// Paint a horizontal span `[left, right)` on row `y`, with optional edge AA.
350fn paint_span(fb: &mut Framebuffer, left: f32, right: f32, y: u32, color: Color, aa: bool) {
351    if y >= fb.height() {
352        return;
353    }
354    let x0 = left.floor() as i32;
355    let x1 = right.ceil() as i32;
356    let Color(cr, cg, cb, ca) = color;
357
358    for px in x0..x1 {
359        if px < 0 || px as u32 >= fb.width() {
360            continue;
361        }
362        let coverage = if aa {
363            span_coverage(left, right, px as f32)
364        } else {
365            // Hard edge: any pixel whose centre is inside the span.
366            let centre = px as f32 + 0.5;
367            if centre >= left && centre < right {
368                1.0
369            } else {
370                0.0
371            }
372        };
373        if coverage <= 0.0 {
374            continue;
375        }
376        let alpha = (ca as f32 * coverage).round() as u8;
377        fb.blend(
378            px as u32,
379            y,
380            crate::framebuffer::pack_rgba(cr, cg, cb, alpha),
381        );
382    }
383}
384
385/// Core clipped-fill implementation that reuses caller-provided scratch buffers.
386fn fill_polygon_clipped_with_scratch(
387    fb: &mut Framebuffer,
388    points: &[(f32, f32)],
389    color: Color,
390    fill_rule: FillRule,
391    aa: bool,
392    clip: crate::clip::ClipRect,
393    scratch: &mut RasterizerScratch,
394) {
395    if points.len() < 3 {
396        return;
397    }
398
399    // Bounding box.
400    let (mut min_y, mut max_y) = (f32::INFINITY, f32::NEG_INFINITY);
401    for &(_, y) in points {
402        if y < min_y {
403            min_y = y;
404        }
405        if y > max_y {
406            max_y = y;
407        }
408    }
409    let y_clip_start = (min_y.floor() as i64).max(clip.y0) as i32;
410    let y_end = (max_y.ceil() as i64).min(clip.y1) as i32;
411
412    scratch.clear();
413    build_edges_into(points, &mut scratch.global_edges);
414    scratch.global_edges.sort_by(|a, b| {
415        a.0.cmp(&b.0).then(
416            a.1.x
417                .partial_cmp(&b.1.x)
418                .unwrap_or(core::cmp::Ordering::Equal),
419        )
420    });
421
422    let mut gi = 0usize;
423
424    for y in (min_y.floor() as i32)..y_end {
425        while gi < scratch.global_edges.len() && scratch.global_edges[gi].0 == y {
426            scratch.active.push(scratch.global_edges[gi].1.clone());
427            gi += 1;
428        }
429        scratch.active.retain(|e| e.y_max > y);
430        scratch
431            .active
432            .sort_by(|a, b| a.x.partial_cmp(&b.x).unwrap_or(core::cmp::Ordering::Equal));
433        if y >= y_clip_start {
434            fill_spans_clipped(fb, &scratch.active, y as u32, color, fill_rule, aa, &clip);
435        }
436        for e in &mut scratch.active {
437            e.x += e.dx;
438        }
439    }
440}
441
442/// Fill a polygon clipped to a [`crate::clip::ClipRect`].
443///
444/// This is the same as [`fill_polygon`] but additionally clips to `clip`.
445///
446/// For tight loops over many polygons, prefer [`Rasterizer::fill_polygon_clipped`]
447/// which reuses scratch buffers across calls.
448pub fn fill_polygon_clipped(
449    fb: &mut Framebuffer,
450    points: &[(f32, f32)],
451    color: Color,
452    fill_rule: FillRule,
453    aa: bool,
454    clip: crate::clip::ClipRect,
455) {
456    let mut scratch = RasterizerScratch::new();
457    fill_polygon_clipped_with_scratch(fb, points, color, fill_rule, aa, clip, &mut scratch);
458}
459
460/// Like `fill_spans` but clips each span to `clip`.
461fn fill_spans_clipped(
462    fb: &mut Framebuffer,
463    active: &[Edge],
464    y: u32,
465    color: Color,
466    fill_rule: FillRule,
467    aa: bool,
468    clip: &crate::clip::ClipRect,
469) {
470    if y >= fb.height() || (y as i64) < clip.y0 || (y as i64) >= clip.y1 || active.is_empty() {
471        return;
472    }
473    match fill_rule {
474        FillRule::EvenOdd => {
475            let mut i = 0;
476            while i + 1 < active.len() {
477                let left = active[i].x.max(clip.x0 as f32);
478                let right = active[i + 1].x.min(clip.x1 as f32);
479                if right > left {
480                    paint_span(fb, left, right, y, color, aa);
481                }
482                i += 2;
483            }
484        }
485        FillRule::NonZero => {
486            let mut winding = 0i32;
487            let mut fill_start: Option<f32> = None;
488            for edge in active {
489                let prev_winding = winding;
490                winding += edge.winding;
491                if prev_winding == 0 && winding != 0 {
492                    fill_start = Some(edge.x.max(clip.x0 as f32));
493                } else if prev_winding != 0 && winding == 0 {
494                    if let Some(start) = fill_start.take() {
495                        let end = edge.x.min(clip.x1 as f32);
496                        if end > start {
497                            paint_span(fb, start, end, y, color, aa);
498                        }
499                    }
500                }
501            }
502        }
503    }
504}
505
506/// Fill a triangle defined by three points into `fb` with coverage AA.
507///
508/// Delegates to [`fill_polygon`] with three vertices.
509pub fn fill_triangle(
510    fb: &mut Framebuffer,
511    p0: (f32, f32),
512    p1: (f32, f32),
513    p2: (f32, f32),
514    color: Color,
515) {
516    fill_polygon(fb, &[p0, p1, p2], color, FillRule::NonZero, true);
517}
518
519// ---------------------------------------------------------------------------
520// Tests
521// ---------------------------------------------------------------------------
522
523#[cfg(test)]
524mod tests {
525    use super::*;
526    use crate::framebuffer::Framebuffer;
527
528    fn fresh(w: u32, h: u32) -> Framebuffer {
529        Framebuffer::with_fill(w, h, Color(0, 0, 0, 255))
530    }
531
532    #[test]
533    fn triangle_fill_coverage() {
534        let mut fb = fresh(20, 20);
535        // Fill a large triangle covering roughly half the buffer.
536        fill_triangle(
537            &mut fb,
538            (0.0, 0.0),
539            (20.0, 0.0),
540            (10.0, 20.0),
541            Color(255, 255, 255, 255),
542        );
543        // Count non-background pixels.
544        let mut count = 0u32;
545        for y in 0..20 {
546            for x in 0..20 {
547                let (r, _, _, _) = fb.get_rgba(x, y).unwrap_or((0, 0, 0, 0));
548                if r > 0 {
549                    count += 1;
550                }
551            }
552        }
553        // The triangle should fill at least 50 pixels in a 20x20 buffer.
554        assert!(count >= 50, "expected >= 50 filled pixels, got {count}");
555    }
556
557    #[test]
558    fn polygon_fill_even_odd() {
559        // 5-pointed star: a self-intersecting polygon where even-odd leaves the
560        // center pentagon empty, while non-zero fills it.
561        //
562        // Use a large star (radius=30, centre=35,35) so individual arms are wide.
563        let cx = 35.0f32;
564        let cy = 35.0f32;
565        let r = 30.0f32;
566        let star: Vec<(f32, f32)> = (0..5)
567            .map(|i| {
568                // "Skip-one" order produces a 5-pointed star.
569                let angle = std::f32::consts::PI * (2.0 * (2 * i) as f32 / 5.0 - 0.5);
570                (cx + r * angle.cos(), cy + r * angle.sin())
571            })
572            .collect();
573
574        let mut fb_eo = fresh(70, 70);
575        let mut fb_nz = fresh(70, 70);
576        fill_polygon(
577            &mut fb_eo,
578            &star,
579            Color(255, 0, 0, 255),
580            FillRule::EvenOdd,
581            false,
582        );
583        fill_polygon(
584            &mut fb_nz,
585            &star,
586            Color(255, 0, 0, 255),
587            FillRule::NonZero,
588            false,
589        );
590
591        // The top arm of the star spans roughly x=[32,38] at y=8 (well inside the arm).
592        let (r_eo_tip, _, _, _) = fb_eo.get_rgba(35, 8).unwrap_or((0, 0, 0, 0));
593        let (r_nz_tip, _, _, _) = fb_nz.get_rgba(35, 8).unwrap_or((0, 0, 0, 0));
594        assert!(r_eo_tip > 0, "EvenOdd: star arm should be painted");
595        assert!(r_nz_tip > 0, "NonZero: star arm should be painted");
596
597        // Center pentagon: NonZero should fill it; EvenOdd should leave it empty.
598        let (r_nz_ctr, _, _, _) = fb_nz.get_rgba(35, 35).unwrap_or((0, 0, 0, 0));
599        assert!(r_nz_ctr > 0, "NonZero: star center must be filled");
600        let (r_eo_ctr, _, _, _) = fb_eo.get_rgba(35, 35).unwrap_or((0, 0, 0, 0));
601        assert_eq!(
602            r_eo_ctr, 0,
603            "EvenOdd: star center should be a hole (r={r_eo_ctr})"
604        );
605    }
606
607    #[test]
608    fn fill_rect_via_polygon() {
609        let mut fb = fresh(10, 10);
610        let pts = [(2.0f32, 2.0), (8.0, 2.0), (8.0, 8.0), (2.0, 8.0)];
611        fill_polygon(
612            &mut fb,
613            &pts,
614            Color(0, 255, 0, 255),
615            FillRule::NonZero,
616            false,
617        );
618        // Centre should be green.
619        assert_eq!(fb.get_rgba(5, 5), Some((0, 255, 0, 255)));
620        // Corner outside should be black.
621        assert_eq!(fb.get_rgba(0, 0), Some((0, 0, 0, 255)));
622    }
623
624    #[test]
625    fn fill_rule_noop_on_empty_polygon() {
626        let mut fb = fresh(5, 5);
627        fill_polygon(
628            &mut fb,
629            &[],
630            Color(255, 0, 0, 255),
631            FillRule::NonZero,
632            false,
633        );
634        // Nothing should change.
635        assert_eq!(fb.get_rgba(2, 2), Some((0, 0, 0, 255)));
636    }
637
638    // -----------------------------------------------------------------------
639    // S2: scanline buffer reuse regression test
640    // -----------------------------------------------------------------------
641
642    #[test]
643    fn test_scanline_reuse_output_identical() {
644        // Regression test: filling a triangle into a 100×100 framebuffer twice
645        // using the `Rasterizer` (scratch-reuse path) must produce byte-identical
646        // results across both calls.  This verifies that `scratch.clear()` fully
647        // resets state between fills so stale edges never contaminate the next fill.
648        let triangle: &[(f32, f32)] = &[(10.0, 90.0), (50.0, 10.0), (90.0, 90.0)];
649        let color = Color(200, 100, 50, 255);
650
651        // First fill — using the reuse-scratch Rasterizer.
652        let mut fb1 = fresh(100, 100);
653        let mut ras = Rasterizer::new();
654        ras.fill_polygon(&mut fb1, triangle, color, FillRule::NonZero, true);
655
656        // Second fill into a fresh buffer — same Rasterizer, scratch is reused.
657        let mut fb2 = fresh(100, 100);
658        ras.fill_polygon(&mut fb2, triangle, color, FillRule::NonZero, true);
659
660        // Results must be byte-identical.
661        for y in 0..100 {
662            for x in 0..100 {
663                let p1 = fb1.get_rgba(x, y);
664                let p2 = fb2.get_rgba(x, y);
665                assert_eq!(
666                    p1, p2,
667                    "pixel ({x},{y}) differs: first={p1:?} second={p2:?}"
668                );
669            }
670        }
671
672        // Also verify the public free-fn path gives the same pixels (API stability).
673        let mut fb3 = fresh(100, 100);
674        fill_polygon(&mut fb3, triangle, color, FillRule::NonZero, true);
675        for y in 0..100 {
676            for x in 0..100 {
677                let p1 = fb1.get_rgba(x, y);
678                let p3 = fb3.get_rgba(x, y);
679                assert_eq!(
680                    p1, p3,
681                    "pixel ({x},{y}): Rasterizer={p1:?} vs free fn={p3:?}"
682                );
683            }
684        }
685    }
686}