1use crate::framebuffer::Framebuffer;
9use oxiui_core::Color;
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
13pub enum FillRule {
14 EvenOdd,
17 #[default]
20 NonZero,
21}
22
23#[derive(Clone, Debug)]
30pub(crate) struct Edge {
31 x: f32,
33 dx: f32,
35 y_max: i32,
37 winding: i32,
40}
41
42pub struct RasterizerScratch {
51 pub(crate) global_edges: Vec<(i32, Edge)>,
53 pub(crate) active: Vec<Edge>,
55}
56
57impl RasterizerScratch {
58 pub fn new() -> Self {
60 Self {
61 global_edges: Vec::new(),
62 active: Vec::new(),
63 }
64 }
65
66 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
79pub struct Rasterizer {
86 scratch: RasterizerScratch,
87}
88
89impl Rasterizer {
90 pub fn new() -> Self {
92 Self {
93 scratch: RasterizerScratch::new(),
94 }
95 }
96
97 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 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
137fn 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 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 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#[inline]
185fn span_coverage(left: f32, right: f32, px: f32) -> f32 {
186 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
197fn 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 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 raw_y_start = min_y.floor() as i32;
224 let raw_y_end = max_y.ceil() as i32;
225
226 let height_i32 = fb.height().min(i32::MAX as u32) as i32;
232 let y_start = raw_y_start.clamp(0, height_i32);
233 let y_end = raw_y_end.clamp(y_start, height_i32);
234
235 scratch.clear();
237 build_edges_into(points, &mut scratch.global_edges);
238
239 scratch.global_edges.sort_by(|a, b| {
241 a.0.cmp(&b.0).then(
242 a.1.x
243 .partial_cmp(&b.1.x)
244 .unwrap_or(core::cmp::Ordering::Equal),
245 )
246 });
247
248 let mut gi = 0usize;
249
250 while gi < scratch.global_edges.len() && scratch.global_edges[gi].0 < y_start {
256 let (edge_y_start, edge) = &scratch.global_edges[gi];
257 if edge.y_max > y_start {
258 let mut e = edge.clone();
259 e.x += e.dx * (y_start - edge_y_start) as f32;
260 scratch.active.push(e);
261 }
262 gi += 1;
263 }
264
265 for y in y_start..y_end {
266 while gi < scratch.global_edges.len() && scratch.global_edges[gi].0 == y {
268 scratch.active.push(scratch.global_edges[gi].1.clone());
269 gi += 1;
270 }
271
272 scratch.active.retain(|e| e.y_max > y);
274
275 scratch
277 .active
278 .sort_by(|a, b| a.x.partial_cmp(&b.x).unwrap_or(core::cmp::Ordering::Equal));
279
280 fill_spans(
282 fb,
283 &scratch.active,
284 y as u32,
285 y as f32,
286 color,
287 fill_rule,
288 aa,
289 );
290
291 for e in &mut scratch.active {
293 e.x += e.dx;
294 }
295 }
296}
297
298pub fn fill_polygon(
309 fb: &mut Framebuffer,
310 points: &[(f32, f32)],
311 color: Color,
312 fill_rule: FillRule,
313 aa: bool,
314) {
315 let mut scratch = RasterizerScratch::new();
316 fill_polygon_with_scratch(fb, points, color, fill_rule, aa, &mut scratch);
317}
318
319fn fill_spans(
321 fb: &mut Framebuffer,
322 active: &[Edge],
323 y: u32,
324 _y_float: f32,
325 color: Color,
326 fill_rule: FillRule,
327 aa: bool,
328) {
329 if y >= fb.height() || active.is_empty() {
330 return;
331 }
332
333 match fill_rule {
334 FillRule::EvenOdd => fill_even_odd(fb, active, y, color, aa),
335 FillRule::NonZero => fill_non_zero(fb, active, y, color, aa),
336 }
337}
338
339fn fill_even_odd(fb: &mut Framebuffer, active: &[Edge], y: u32, color: Color, aa: bool) {
341 let mut i = 0;
342 while i + 1 < active.len() {
343 let left = active[i].x;
344 let right = active[i + 1].x;
345 if right > left {
346 paint_span(fb, left, right, y, color, aa);
347 }
348 i += 2;
349 }
350}
351
352fn fill_non_zero(fb: &mut Framebuffer, active: &[Edge], y: u32, color: Color, aa: bool) {
354 let mut winding = 0i32;
355 let mut fill_start: Option<f32> = None;
356
357 for edge in active {
358 let prev_winding = winding;
359 winding += edge.winding;
360
361 if prev_winding == 0 && winding != 0 {
362 fill_start = Some(edge.x);
364 } else if prev_winding != 0 && winding == 0 {
365 if let Some(start) = fill_start.take() {
367 let end = edge.x;
368 if end > start {
369 paint_span(fb, start, end, y, color, aa);
370 }
371 }
372 }
373 }
374}
375
376fn paint_span(fb: &mut Framebuffer, left: f32, right: f32, y: u32, color: Color, aa: bool) {
378 if y >= fb.height() {
379 return;
380 }
381 let width_i32 = fb.width().min(i32::MAX as u32) as i32;
387 let x0 = (left.floor() as i32).clamp(0, width_i32);
388 let x1 = (right.ceil() as i32).clamp(x0, width_i32);
389 let Color(cr, cg, cb, ca) = color;
390
391 for px in x0..x1 {
392 let coverage = if aa {
394 span_coverage(left, right, px as f32)
395 } else {
396 let centre = px as f32 + 0.5;
398 if centre >= left && centre < right {
399 1.0
400 } else {
401 0.0
402 }
403 };
404 if coverage <= 0.0 {
405 continue;
406 }
407 let alpha = (ca as f32 * coverage).round() as u8;
408 fb.blend(
409 px as u32,
410 y,
411 crate::framebuffer::pack_rgba(cr, cg, cb, alpha),
412 );
413 }
414}
415
416fn fill_polygon_clipped_with_scratch(
418 fb: &mut Framebuffer,
419 points: &[(f32, f32)],
420 color: Color,
421 fill_rule: FillRule,
422 aa: bool,
423 clip: crate::clip::ClipRect,
424 scratch: &mut RasterizerScratch,
425) {
426 if points.len() < 3 {
427 return;
428 }
429
430 let (mut min_y, mut max_y) = (f32::INFINITY, f32::NEG_INFINITY);
432 for &(_, y) in points {
433 if y < min_y {
434 min_y = y;
435 }
436 if y > max_y {
437 max_y = y;
438 }
439 }
440 let raw_y_start = min_y.floor() as i32;
444 let raw_y_end = max_y.ceil() as i32;
445
446 let height_i32 = fb.height().min(i32::MAX as u32) as i32;
453 let clip_y0 = clip.y0.clamp(0, height_i32 as i64) as i32;
454 let clip_y1 = clip.y1.clamp(0, height_i32 as i64) as i32;
455 let y_clip_start = raw_y_start.max(clip_y0);
456 let y_end = raw_y_end.min(clip_y1);
457
458 scratch.clear();
459 build_edges_into(points, &mut scratch.global_edges);
460 scratch.global_edges.sort_by(|a, b| {
461 a.0.cmp(&b.0).then(
462 a.1.x
463 .partial_cmp(&b.1.x)
464 .unwrap_or(core::cmp::Ordering::Equal),
465 )
466 });
467
468 let mut gi = 0usize;
469
470 while gi < scratch.global_edges.len() && scratch.global_edges[gi].0 < y_clip_start {
475 let (edge_y_start, edge) = &scratch.global_edges[gi];
476 if edge.y_max > y_clip_start {
477 let mut e = edge.clone();
478 e.x += e.dx * (y_clip_start - edge_y_start) as f32;
479 scratch.active.push(e);
480 }
481 gi += 1;
482 }
483
484 for y in y_clip_start..y_end {
485 while gi < scratch.global_edges.len() && scratch.global_edges[gi].0 == y {
486 scratch.active.push(scratch.global_edges[gi].1.clone());
487 gi += 1;
488 }
489 scratch.active.retain(|e| e.y_max > y);
490 scratch
491 .active
492 .sort_by(|a, b| a.x.partial_cmp(&b.x).unwrap_or(core::cmp::Ordering::Equal));
493 fill_spans_clipped(fb, &scratch.active, y as u32, color, fill_rule, aa, &clip);
494 for e in &mut scratch.active {
495 e.x += e.dx;
496 }
497 }
498}
499
500pub fn fill_polygon_clipped(
507 fb: &mut Framebuffer,
508 points: &[(f32, f32)],
509 color: Color,
510 fill_rule: FillRule,
511 aa: bool,
512 clip: crate::clip::ClipRect,
513) {
514 let mut scratch = RasterizerScratch::new();
515 fill_polygon_clipped_with_scratch(fb, points, color, fill_rule, aa, clip, &mut scratch);
516}
517
518fn fill_spans_clipped(
520 fb: &mut Framebuffer,
521 active: &[Edge],
522 y: u32,
523 color: Color,
524 fill_rule: FillRule,
525 aa: bool,
526 clip: &crate::clip::ClipRect,
527) {
528 if y >= fb.height() || (y as i64) < clip.y0 || (y as i64) >= clip.y1 || active.is_empty() {
529 return;
530 }
531 match fill_rule {
532 FillRule::EvenOdd => {
533 let mut i = 0;
534 while i + 1 < active.len() {
535 let left = active[i].x.max(clip.x0 as f32);
536 let right = active[i + 1].x.min(clip.x1 as f32);
537 if right > left {
538 paint_span(fb, left, right, y, color, aa);
539 }
540 i += 2;
541 }
542 }
543 FillRule::NonZero => {
544 let mut winding = 0i32;
545 let mut fill_start: Option<f32> = None;
546 for edge in active {
547 let prev_winding = winding;
548 winding += edge.winding;
549 if prev_winding == 0 && winding != 0 {
550 fill_start = Some(edge.x.max(clip.x0 as f32));
551 } else if prev_winding != 0 && winding == 0 {
552 if let Some(start) = fill_start.take() {
553 let end = edge.x.min(clip.x1 as f32);
554 if end > start {
555 paint_span(fb, start, end, y, color, aa);
556 }
557 }
558 }
559 }
560 }
561 }
562}
563
564pub fn fill_triangle(
568 fb: &mut Framebuffer,
569 p0: (f32, f32),
570 p1: (f32, f32),
571 p2: (f32, f32),
572 color: Color,
573) {
574 fill_polygon(fb, &[p0, p1, p2], color, FillRule::NonZero, true);
575}
576
577#[cfg(test)]
582mod tests {
583 use super::*;
584 use crate::framebuffer::Framebuffer;
585
586 fn fresh(w: u32, h: u32) -> Framebuffer {
587 Framebuffer::with_fill(w, h, Color(0, 0, 0, 255))
588 }
589
590 #[test]
591 fn triangle_fill_coverage() {
592 let mut fb = fresh(20, 20);
593 fill_triangle(
595 &mut fb,
596 (0.0, 0.0),
597 (20.0, 0.0),
598 (10.0, 20.0),
599 Color(255, 255, 255, 255),
600 );
601 let mut count = 0u32;
603 for y in 0..20 {
604 for x in 0..20 {
605 let (r, _, _, _) = fb.get_rgba(x, y).unwrap_or((0, 0, 0, 0));
606 if r > 0 {
607 count += 1;
608 }
609 }
610 }
611 assert!(count >= 50, "expected >= 50 filled pixels, got {count}");
613 }
614
615 #[test]
616 fn polygon_fill_even_odd() {
617 let cx = 35.0f32;
622 let cy = 35.0f32;
623 let r = 30.0f32;
624 let star: Vec<(f32, f32)> = (0..5)
625 .map(|i| {
626 let angle = std::f32::consts::PI * (2.0 * (2 * i) as f32 / 5.0 - 0.5);
628 (cx + r * angle.cos(), cy + r * angle.sin())
629 })
630 .collect();
631
632 let mut fb_eo = fresh(70, 70);
633 let mut fb_nz = fresh(70, 70);
634 fill_polygon(
635 &mut fb_eo,
636 &star,
637 Color(255, 0, 0, 255),
638 FillRule::EvenOdd,
639 false,
640 );
641 fill_polygon(
642 &mut fb_nz,
643 &star,
644 Color(255, 0, 0, 255),
645 FillRule::NonZero,
646 false,
647 );
648
649 let (r_eo_tip, _, _, _) = fb_eo.get_rgba(35, 8).unwrap_or((0, 0, 0, 0));
651 let (r_nz_tip, _, _, _) = fb_nz.get_rgba(35, 8).unwrap_or((0, 0, 0, 0));
652 assert!(r_eo_tip > 0, "EvenOdd: star arm should be painted");
653 assert!(r_nz_tip > 0, "NonZero: star arm should be painted");
654
655 let (r_nz_ctr, _, _, _) = fb_nz.get_rgba(35, 35).unwrap_or((0, 0, 0, 0));
657 assert!(r_nz_ctr > 0, "NonZero: star center must be filled");
658 let (r_eo_ctr, _, _, _) = fb_eo.get_rgba(35, 35).unwrap_or((0, 0, 0, 0));
659 assert_eq!(
660 r_eo_ctr, 0,
661 "EvenOdd: star center should be a hole (r={r_eo_ctr})"
662 );
663 }
664
665 #[test]
666 fn fill_rect_via_polygon() {
667 let mut fb = fresh(10, 10);
668 let pts = [(2.0f32, 2.0), (8.0, 2.0), (8.0, 8.0), (2.0, 8.0)];
669 fill_polygon(
670 &mut fb,
671 &pts,
672 Color(0, 255, 0, 255),
673 FillRule::NonZero,
674 false,
675 );
676 assert_eq!(fb.get_rgba(5, 5), Some((0, 255, 0, 255)));
678 assert_eq!(fb.get_rgba(0, 0), Some((0, 0, 0, 255)));
680 }
681
682 #[test]
683 fn fill_rule_noop_on_empty_polygon() {
684 let mut fb = fresh(5, 5);
685 fill_polygon(
686 &mut fb,
687 &[],
688 Color(255, 0, 0, 255),
689 FillRule::NonZero,
690 false,
691 );
692 assert_eq!(fb.get_rgba(2, 2), Some((0, 0, 0, 255)));
694 }
695
696 #[test]
701 fn test_scanline_reuse_output_identical() {
702 let triangle: &[(f32, f32)] = &[(10.0, 90.0), (50.0, 10.0), (90.0, 90.0)];
707 let color = Color(200, 100, 50, 255);
708
709 let mut fb1 = fresh(100, 100);
711 let mut ras = Rasterizer::new();
712 ras.fill_polygon(&mut fb1, triangle, color, FillRule::NonZero, true);
713
714 let mut fb2 = fresh(100, 100);
716 ras.fill_polygon(&mut fb2, triangle, color, FillRule::NonZero, true);
717
718 for y in 0..100 {
720 for x in 0..100 {
721 let p1 = fb1.get_rgba(x, y);
722 let p2 = fb2.get_rgba(x, y);
723 assert_eq!(
724 p1, p2,
725 "pixel ({x},{y}) differs: first={p1:?} second={p2:?}"
726 );
727 }
728 }
729
730 let mut fb3 = fresh(100, 100);
732 fill_polygon(&mut fb3, triangle, color, FillRule::NonZero, true);
733 for y in 0..100 {
734 for x in 0..100 {
735 let p1 = fb1.get_rgba(x, y);
736 let p3 = fb3.get_rgba(x, y);
737 assert_eq!(
738 p1, p3,
739 "pixel ({x},{y}): Rasterizer={p1:?} vs free fn={p3:?}"
740 );
741 }
742 }
743 }
744
745 #[test]
750 fn fill_polygon_far_out_of_bounds_is_clamped_and_fast() {
751 let mut fb = fresh(10, 10);
759 let huge = 1.0e9_f32;
760 let pts = [(-huge, -huge), (huge, -huge), (huge, huge), (-huge, huge)];
761 let start = std::time::Instant::now();
762 fill_polygon(
763 &mut fb,
764 &pts,
765 Color(255, 0, 0, 255),
766 FillRule::NonZero,
767 false,
768 );
769 let elapsed = start.elapsed();
770 assert!(
771 elapsed < std::time::Duration::from_secs(2),
772 "fill_polygon took {elapsed:?} for far-out-of-bounds coordinates; \
773 the vertical/horizontal span is not being clamped to the framebuffer"
774 );
775 for y in 0..10 {
778 for x in 0..10 {
779 assert_eq!(
780 fb.get_rgba(x, y),
781 Some((255, 0, 0, 255)),
782 "pixel ({x},{y}) should be filled"
783 );
784 }
785 }
786 }
787
788 #[test]
789 fn fill_polygon_clipped_far_out_of_bounds_is_clamped_and_fast() {
790 let mut fb = fresh(10, 10);
793 let huge = 1.0e9_f32;
794 let pts = [(-huge, -huge), (huge, -huge), (huge, huge), (-huge, huge)];
795 let clip = crate::clip::ClipRect::full(10, 10);
796 let start = std::time::Instant::now();
797 fill_polygon_clipped(
798 &mut fb,
799 &pts,
800 Color(0, 0, 255, 255),
801 FillRule::NonZero,
802 false,
803 clip,
804 );
805 let elapsed = start.elapsed();
806 assert!(
807 elapsed < std::time::Duration::from_secs(2),
808 "fill_polygon_clipped took {elapsed:?} for far-out-of-bounds coordinates"
809 );
810 for y in 0..10 {
811 for x in 0..10 {
812 assert_eq!(fb.get_rgba(x, y), Some((0, 0, 255, 255)));
813 }
814 }
815 }
816
817 #[test]
818 fn paint_span_far_out_of_bounds_x_is_clamped() {
819 let mut fb = fresh(8, 8);
823 let start = std::time::Instant::now();
824 paint_span(&mut fb, -1.0e9, 1.0e9, 3, Color(10, 20, 30, 255), false);
825 let elapsed = start.elapsed();
826 assert!(
827 elapsed < std::time::Duration::from_secs(2),
828 "paint_span took {elapsed:?} for a far-out-of-bounds span"
829 );
830 for x in 0..8 {
833 assert_eq!(fb.get_rgba(x, 3), Some((10, 20, 30, 255)));
834 }
835 assert_eq!(fb.get_rgba(0, 0), Some((0, 0, 0, 255)));
836 }
837
838 #[test]
839 fn fill_polygon_partial_offscreen_top_still_paints_visible_rows() {
840 let mut fb = fresh(10, 10);
846 let pts = [(2.0f32, -1000.0), (8.0, -1000.0), (8.0, 5.0), (2.0, 5.0)];
847 fill_polygon(
848 &mut fb,
849 &pts,
850 Color(0, 255, 0, 255),
851 FillRule::NonZero,
852 false,
853 );
854 for y in 0..5 {
855 assert_eq!(
856 fb.get_rgba(5, y),
857 Some((0, 255, 0, 255)),
858 "row {y} should be filled by the far-off-screen rectangle"
859 );
860 }
861 assert_eq!(fb.get_rgba(5, 9), Some((0, 0, 0, 255)));
863 }
864}