Skip to main content

embedded_3dgfx/
draw.rs

1// Row width configuration - features are prioritized if multiple are enabled
2#[cfg(feature = "row_width_320")]
3const MAX_ROW_WIDTH: usize = 320;
4#[cfg(all(feature = "row_width_240", not(feature = "row_width_320")))]
5const MAX_ROW_WIDTH: usize = 240;
6#[cfg(all(
7    feature = "row_width_160",
8    not(feature = "row_width_240"),
9    not(feature = "row_width_320"),
10    not(feature = "row_width_96")
11))]
12const MAX_ROW_WIDTH: usize = 160;
13#[cfg(all(
14    feature = "row_width_96",
15    not(feature = "row_width_160"),
16    not(feature = "row_width_240"),
17    not(feature = "row_width_320")
18))]
19const MAX_ROW_WIDTH: usize = 96;
20#[cfg(not(any(
21    feature = "row_width_320",
22    feature = "row_width_240",
23    feature = "row_width_160",
24    feature = "row_width_96"
25)))]
26const MAX_ROW_WIDTH: usize = 100;
27
28use core::fmt::Debug;
29use embedded_graphics_core::draw_target::DrawTarget;
30#[cfg(feature = "aa")]
31use embedded_graphics_core::pixelcolor::Rgb565;
32use embedded_graphics_core::pixelcolor::RgbColor;
33use embedded_graphics_core::prelude::Point;
34use heapless::Vec;
35
36use crate::DrawPrimitive;
37
38/// Framebuffer that supports reading back pixel values.
39///
40/// Required by the analytical-AA rasterizers (`draw_zbuffered_aa`,
41/// `draw_line_aa`) which blend partial-coverage triangle edges and
42/// anti-aliased line endpoints with the existing framebuffer contents.
43/// Implementers should return the most recently written color at `point`,
44/// or `Rgb565::BLACK` for out-of-bounds reads.
45#[cfg(feature = "aa")]
46pub trait ReadPixel {
47    fn read_pixel(&self, point: Point) -> Rgb565;
48}
49
50/// Component-wise blend in 8-bit fixed-point coverage.
51/// `coverage_q8` ∈ [0, 256]; 256 = full triangle color, 0 = full background.
52#[cfg(feature = "aa")]
53#[inline(always)]
54fn blend_q8(bg: Rgb565, fg: Rgb565, coverage_q8: u32) -> Rgb565 {
55    let inv = 256 - coverage_q8;
56    let r = (bg.r() as u32 * inv + fg.r() as u32 * coverage_q8) >> 8;
57    let g = (bg.g() as u32 * inv + fg.g() as u32 * coverage_q8) >> 8;
58    let b = (bg.b() as u32 * inv + fg.b() as u32 * coverage_q8) >> 8;
59    Rgb565::new(r as u8, g as u8, b as u8)
60}
61
62/// Z-test + coverage blend + write a single AA pixel.
63///
64/// Coverage handling has three cases:
65/// - Full coverage (`coverage_q8 >= 256`): fast path, write color directly.
66/// - Partial coverage on a virgin pixel (z-buffer at `u32::MAX`): true
67///   silhouette against the background — blend `bg * (1-c) + color * c`.
68/// - Partial coverage on a pixel another triangle has already painted:
69///   treat as a shared interior edge and write full color. This avoids the
70///   classic double-blend seam artifact at shared edges in closed meshes.
71///   Tradeoff: thin protrusions whose silhouette overlaps another triangle
72///   lose AA on that overlap. Acceptable for typical closed geometry.
73#[cfg(feature = "aa-heuristic")]
74#[inline(always)]
75fn aa_pixel<D>(
76    fb: &mut D,
77    x: i32,
78    y: i32,
79    color: Rgb565,
80    z: u32,
81    zbuffer: &mut [u32],
82    width: usize,
83    coverage_q8: u32,
84) where
85    D: DrawTarget<Color = Rgb565> + ReadPixel,
86    <D as DrawTarget>::Error: Debug,
87{
88    if x < 0 || y < 0 || coverage_q8 == 0 {
89        return;
90    }
91    let idx = y as usize * width + x as usize;
92    if idx >= zbuffer.len() {
93        return;
94    }
95    if z >= zbuffer[idx].saturating_add(DEPTH_EPSILON) {
96        return;
97    }
98
99    let pixel_was_virgin = zbuffer[idx] == u32::MAX;
100    let final_color = if coverage_q8 >= 256 || !pixel_was_virgin {
101        color
102    } else {
103        let bg = fb.read_pixel(Point::new(x, y));
104        blend_q8(bg, color, coverage_q8)
105    };
106    zbuffer[idx] = z;
107    fb.draw_iter([embedded_graphics_core::Pixel(Point::new(x, y), final_color)])
108        .unwrap();
109}
110
111/// Depth epsilon for Z-buffer comparison to prevent Z-fighting
112///
113/// When triangles are nearly coplanar or edge-on to the camera, floating-point
114/// precision errors can cause depth values to be extremely close, leading to
115/// flickering (Z-fighting). This epsilon provides a small bias that helps
116/// new pixels pass the depth test when they are very close to existing ones.
117///
118/// Value: 128 in 16.16 fixed-point format = 0.00195 in floating-point
119/// Tuned for typical embedded graphics scenarios. Increase if Z-fighting persists,
120/// decrease if you notice incorrect depth ordering on distant objects.
121///
122/// **Tuning guide:**
123/// - More Z-fighting? Increase this value (256, 512, etc.)
124/// - Incorrect depth ordering? Decrease this value (64, 32, etc.)
125/// - Adjust camera near/far planes for better depth precision distribution
126const DEPTH_EPSILON: u32 = 128;
127
128/// Configuration for depth-based fog effect
129#[derive(Debug, Clone, Copy)]
130pub struct FogConfig {
131    /// Fog color to blend towards
132    pub color: embedded_graphics_core::pixelcolor::Rgb565,
133    /// Near plane distance (fixed-point 16.16 format)
134    pub near: u32,
135    /// Far plane distance (fixed-point 16.16 format)
136    pub far: u32,
137}
138
139impl FogConfig {
140    /// Create a new fog configuration
141    ///
142    /// # Arguments
143    /// * `color` - The fog color
144    /// * `near` - Near distance (depth values closer than this have no fog)
145    /// * `far` - Far distance (depth values farther than this are fully fogged)
146    pub fn new(color: embedded_graphics_core::pixelcolor::Rgb565, near: f32, far: f32) -> Self {
147        Self {
148            color,
149            near: (near * 65536.0) as u32,
150            far: (far * 65536.0) as u32,
151        }
152    }
153
154    /// Apply fog effect to a color based on depth
155    #[inline]
156    pub fn apply(
157        &self,
158        base_color: embedded_graphics_core::pixelcolor::Rgb565,
159        depth: u32,
160    ) -> embedded_graphics_core::pixelcolor::Rgb565 {
161        // Calculate fog factor: 0.0 at near plane, 1.0 at far plane
162        let fog_factor = if depth <= self.near {
163            0u32
164        } else if depth >= self.far {
165            65536u32 // 1.0 in fixed-point
166        } else {
167            // Linear interpolation: (depth - near) / (far - near)
168            let numerator = (depth - self.near) as u64;
169            let denominator = (self.far - self.near) as u64;
170            ((numerator * 65536) / denominator) as u32
171        };
172
173        // Blend base color with fog color
174        // final_color = base_color * (1 - fog_factor) + fog_color * fog_factor
175        let base_r = base_color.r() as u32;
176        let base_g = base_color.g() as u32;
177        let base_b = base_color.b() as u32;
178
179        let fog_r = self.color.r() as u32;
180        let fog_g = self.color.g() as u32;
181        let fog_b = self.color.b() as u32;
182
183        // fog_factor is in 16.16 fixed-point format
184        let r = ((base_r * (65536 - fog_factor) + fog_r * fog_factor) / 65536) as u8;
185        let g = ((base_g * (65536 - fog_factor) + fog_g * fog_factor) / 65536) as u8;
186        let b = ((base_b * (65536 - fog_factor) + fog_b * fog_factor) / 65536) as u8;
187
188        embedded_graphics_core::pixelcolor::Rgb565::new(r, g, b)
189    }
190}
191
192/// Configuration for ordered dithering effect
193#[derive(Debug, Clone, Copy)]
194pub struct DitherConfig {
195    /// Dithering intensity (0-255, where 0 is no dithering)
196    pub intensity: u8,
197}
198
199impl DitherConfig {
200    /// 4x4 Bayer matrix for ordered dithering
201    /// Values are in range [0, 15] and will be scaled by intensity
202    const BAYER_MATRIX: [[u8; 4]; 4] =
203        [[0, 8, 2, 10], [12, 4, 14, 6], [3, 11, 1, 9], [15, 7, 13, 5]];
204
205    /// Create a new dither configuration
206    pub fn new(intensity: u8) -> Self {
207        Self { intensity }
208    }
209
210    /// Apply dithering effect to a color based on screen position
211    #[inline]
212    pub fn apply(
213        &self,
214        color: embedded_graphics_core::pixelcolor::Rgb565,
215        x: i32,
216        y: i32,
217    ) -> embedded_graphics_core::pixelcolor::Rgb565 {
218        if self.intensity == 0 {
219            return color;
220        }
221
222        // Get threshold from Bayer matrix (tiles every 4x4 pixels)
223        let matrix_x = (x & 3) as usize;
224        let matrix_y = (y & 3) as usize;
225        let threshold = Self::BAYER_MATRIX[matrix_y][matrix_x];
226
227        // Scale threshold by intensity
228        // threshold is 0-15, intensity is 0-255
229        // Combined threshold is in range 0-255
230        let scaled_threshold = ((threshold as u16 * self.intensity as u16) / 15) as u8;
231
232        // Apply threshold to each color channel
233        let r = color.r();
234        let g = color.g();
235        let b = color.b();
236
237        // Add dithering noise (can increase or decrease based on threshold)
238        let r = if r > scaled_threshold {
239            r.saturating_sub(scaled_threshold / 2)
240        } else {
241            r.saturating_add(scaled_threshold / 2)
242        };
243
244        let g = if g > scaled_threshold {
245            g.saturating_sub(scaled_threshold / 2)
246        } else {
247            g.saturating_add(scaled_threshold / 2)
248        };
249
250        let b = if b > scaled_threshold {
251            b.saturating_sub(scaled_threshold / 2)
252        } else {
253            b.saturating_add(scaled_threshold / 2)
254        };
255
256        embedded_graphics_core::pixelcolor::Rgb565::new(r, g, b)
257    }
258}
259
260struct Interpolator {
261    x: i32,
262    dx: i32,
263    dy: i32,
264    error: i32,
265}
266
267impl Interpolator {
268    fn new(p_start: Point, p_end: Point) -> Self {
269        Self {
270            x: p_start.x,
271            dx: p_end.x - p_start.x,
272            dy: p_end.y - p_start.y,
273            error: 0,
274        }
275    }
276
277    fn next(&mut self) -> i32 {
278        self.x += self.dx / self.dy;
279        self.error += self.dx % self.dy;
280        if self.error >= self.dy {
281            self.x += 1;
282            self.error -= self.dy;
283        }
284        self.x
285    }
286}
287
288// Fixed-point 16.16 edge stepper — integer-only replacement for f32 invslope.
289const FP_SHIFT: i64 = 16;
290
291#[inline(always)]
292fn fixed_to_i32(value: i64) -> i32 {
293    if value >= 0 {
294        (value >> FP_SHIFT) as i32
295    } else {
296        -((-value) >> FP_SHIFT) as i32
297    }
298}
299
300struct EdgeStepper {
301    x: i64,
302    step: i64,
303}
304
305impl EdgeStepper {
306    fn new(start: Point, end: Point, y: i32) -> Self {
307        let dy = (end.y - start.y) as i64;
308        let (step, x) = if dy != 0 {
309            let s = (((end.x - start.x) as i64) << FP_SHIFT) / dy;
310            let x = ((start.x as i64) << FP_SHIFT) + s * (y - start.y) as i64;
311            (s, x)
312        } else {
313            (0, (start.x as i64) << FP_SHIFT)
314        };
315        Self { x, step }
316    }
317
318    #[inline(always)]
319    fn current_x(&self) -> i32 {
320        fixed_to_i32(self.x)
321    }
322
323    #[inline(always)]
324    fn advance(&mut self) {
325        self.x += self.step;
326    }
327}
328
329#[inline(always)]
330fn fill_triangle<D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>>(
331    p1: Point,
332    p2: Point,
333    p3: Point,
334    color: embedded_graphics_core::pixelcolor::Rgb565,
335    fb: &mut D,
336) where
337    <D as DrawTarget>::Error: Debug,
338{
339    let area = (p2.x - p1.x) * (p3.y - p1.y) - (p2.y - p1.y) * (p3.x - p1.x);
340    if area == 0 {
341        // Degenerate triangle (all points collinear)
342        return;
343    }
344
345    let bounds = fb.bounding_box();
346    let min_x = bounds.top_left.x;
347    let max_x = bounds.bottom_right().unwrap().x;
348
349    let mut pixel_row: [embedded_graphics_core::Pixel<embedded_graphics_core::pixelcolor::Rgb565>;
350        MAX_ROW_WIDTH] = [embedded_graphics_core::Pixel(
351        Point::new(0, 0),
352        embedded_graphics_core::pixelcolor::RgbColor::BLACK,
353    ); MAX_ROW_WIDTH];
354
355    // Top part (p1 to p2)
356    if p2.y - p1.y > 0 {
357        let mut a = Interpolator::new(p1, p2);
358        let mut b = Interpolator::new(p1, p3);
359
360        for y in p1.y..p2.y {
361            let ax = a.next();
362            let bx = b.next();
363            let (start_x, end_x) = if ax < bx { (ax, bx) } else { (bx, ax) };
364            let start_x = start_x.clamp(min_x, max_x);
365            let end_x = end_x.clamp(min_x, max_x);
366
367            let mut i = 0;
368            for x in start_x..=end_x {
369                pixel_row[i] = embedded_graphics_core::Pixel(Point::new(x, y), color);
370                i += 1;
371            }
372
373            fb.draw_iter(pixel_row[..(end_x - start_x + 1) as usize].iter().copied())
374                .unwrap();
375        }
376    }
377
378    // Bottom part (p2 to p3)
379    if p3.y - p2.y > 0 {
380        let mut a = Interpolator::new(p2, p3);
381        let mut b = Interpolator::new(p1, p3);
382
383        for y in p2.y..=p3.y {
384            let ax = a.next();
385            let bx = b.next();
386            let (start_x, end_x) = if ax < bx { (ax, bx) } else { (bx, ax) };
387            let start_x = start_x.clamp(min_x, max_x);
388            let end_x = end_x.clamp(min_x, max_x);
389
390            let mut i = 0;
391            for x in start_x..=end_x {
392                pixel_row[i] = embedded_graphics_core::Pixel(Point::new(x, y), color);
393                i += 1;
394            }
395
396            fb.draw_iter(pixel_row[..(end_x - start_x + 1) as usize].iter().copied())
397                .unwrap();
398        }
399    }
400}
401
402fn fill_bottom_flat_triangle<D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>>(
403    p1: Point,
404    p2: Point,
405    p3: Point,
406    color: embedded_graphics_core::pixelcolor::Rgb565,
407    fb: &mut D,
408) where
409    <D as DrawTarget>::Error: Debug,
410{
411    let mut edge1 = EdgeStepper::new(p1, p2, p1.y);
412    let mut edge2 = EdgeStepper::new(p1, p3, p1.y);
413
414    for scanline_y in p1.y..=p2.y {
415        draw_horizontal_line(
416            Point::new(edge1.current_x(), scanline_y),
417            Point::new(edge2.current_x(), scanline_y),
418            color,
419            fb,
420        );
421        edge1.advance();
422        edge2.advance();
423    }
424}
425
426fn fill_top_flat_triangle<D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>>(
427    p1: Point,
428    p2: Point,
429    p3: Point,
430    color: embedded_graphics_core::pixelcolor::Rgb565,
431    fb: &mut D,
432) where
433    <D as DrawTarget>::Error: Debug,
434{
435    // p1.y == p2.y (top flat), p3 is the bottom vertex; iterate top-down.
436    let mut edge1 = EdgeStepper::new(p1, p3, p1.y);
437    let mut edge2 = EdgeStepper::new(p2, p3, p1.y);
438
439    for scanline_y in p1.y..=p3.y {
440        draw_horizontal_line(
441            Point::new(edge1.current_x(), scanline_y),
442            Point::new(edge2.current_x(), scanline_y),
443            color,
444            fb,
445        );
446        edge1.advance();
447        edge2.advance();
448    }
449}
450
451fn draw_horizontal_line<D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>>(
452    p1: Point,
453    p2: Point,
454    color: embedded_graphics_core::pixelcolor::Rgb565,
455    fb: &mut D,
456) where
457    <D as DrawTarget>::Error: Debug,
458{
459    let start = p1.x.min(p2.x);
460    let end = p1.x.max(p2.x);
461
462    for x in start..=end {
463        fb.draw_iter([embedded_graphics_core::Pixel(Point::new(x, p1.y), color)])
464            .unwrap();
465    }
466}
467
468#[inline]
469pub fn draw<D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>>(
470    primitive: DrawPrimitive,
471    fb: &mut D,
472) where
473    <D as DrawTarget>::Error: Debug,
474{
475    match primitive {
476        DrawPrimitive::Line([p1, p2], color) => {
477            fb.draw_iter(
478                line_drawing::Bresenham::new((p1.x, p1.y), (p2.x, p2.y))
479                    .map(|(x, y)| embedded_graphics_core::Pixel(Point::new(x, y), color)),
480            )
481            .unwrap();
482        }
483        DrawPrimitive::ColoredPoint(p, c) => {
484            let p = embedded_graphics_core::geometry::Point::new(p.x, p.y);
485
486            fb.draw_iter([embedded_graphics_core::Pixel(p, c)]).unwrap();
487        }
488        DrawPrimitive::ColoredTriangle(mut vertices, color) => {
489            // sort vertices by y using sort_unstable_by
490            vertices
491                .as_mut_slice()
492                .sort_unstable_by(|a, b| a.y.cmp(&b.y));
493
494            let [p1, p2, p3] = [
495                Point::new(vertices[0].x, vertices[0].y),
496                Point::new(vertices[1].x, vertices[1].y),
497                Point::new(vertices[2].x, vertices[2].y),
498            ];
499
500            // Off-screen culling: skip if all vertices are beyond the same edge.
501            let bounds = fb.bounding_box();
502            let scr_w = bounds.size.width as i32;
503            let scr_h = bounds.size.height as i32;
504            if p1.x < 0 && p2.x < 0 && p3.x < 0 {
505                return;
506            }
507            if p1.x >= scr_w && p2.x >= scr_w && p3.x >= scr_w {
508                return;
509            }
510            if p1.y < 0 && p2.y < 0 && p3.y < 0 {
511                return;
512            }
513            if p1.y >= scr_h && p2.y >= scr_h && p3.y >= scr_h {
514                return;
515            }
516
517            fill_triangle(p1, p2, p3, color, fb);
518        }
519        DrawPrimitive::ColoredTriangleWithDepth {
520            points,
521            depths: _,
522            color,
523        } => {
524            // This variant should use draw_zbuffered() instead
525            // For compatibility, render without Z-buffering (ignoring depths)
526            let mut vertices = points;
527            if vertices[0].y > vertices[1].y {
528                vertices.swap(0, 1);
529            }
530            if vertices[0].y > vertices[2].y {
531                vertices.swap(0, 2);
532            }
533            if vertices[1].y > vertices[2].y {
534                vertices.swap(1, 2);
535            }
536
537            let mut buf: Vec<_, 3> = Vec::new();
538            for p in vertices.iter() {
539                buf.push(embedded_graphics_core::geometry::Point::new(p.x, p.y))
540                    .unwrap();
541            }
542            let [p1, p2, p3] = buf.into_array().unwrap();
543
544            // Off-screen culling.
545            let bounds = fb.bounding_box();
546            let scr_w = bounds.size.width as i32;
547            let scr_h = bounds.size.height as i32;
548            if p1.x < 0 && p2.x < 0 && p3.x < 0 {
549                return;
550            }
551            if p1.x >= scr_w && p2.x >= scr_w && p3.x >= scr_w {
552                return;
553            }
554            if p1.y < 0 && p2.y < 0 && p3.y < 0 {
555                return;
556            }
557            if p1.y >= scr_h && p2.y >= scr_h && p3.y >= scr_h {
558                return;
559            }
560
561            if p2.y == p3.y {
562                fill_bottom_flat_triangle(p1, p2, p3, color, fb);
563            } else if p1.y == p2.y {
564                fill_top_flat_triangle(p1, p2, p3, color, fb);
565            } else {
566                let p4 = Point::new(
567                    (p1.x as f32
568                        + ((p2.y - p1.y) as f32 / (p3.y - p1.y) as f32) * (p3.x - p1.x) as f32)
569                        as i32,
570                    p2.y,
571                );
572
573                fill_bottom_flat_triangle(p1, p2, p4, color, fb);
574                fill_top_flat_triangle(p2, p4, p3, color, fb);
575            }
576        }
577        DrawPrimitive::GouraudTriangle {
578            mut points,
579            mut colors,
580        } => {
581            // Sort vertices by y coordinate (and corresponding colors)
582            if points[0].y > points[1].y {
583                points.swap(0, 1);
584                colors.swap(0, 1);
585            }
586            if points[0].y > points[2].y {
587                points.swap(0, 2);
588                colors.swap(0, 2);
589            }
590            if points[1].y > points[2].y {
591                points.swap(1, 2);
592                colors.swap(1, 2);
593            }
594
595            let mut buf: Vec<_, 3> = Vec::new();
596            for p in points.iter() {
597                buf.push(embedded_graphics_core::geometry::Point::new(p.x, p.y))
598                    .unwrap();
599            }
600            let [p1, p2, p3] = buf.into_array().unwrap();
601            let [c1, c2, c3] = colors;
602
603            // Off-screen culling.
604            let bounds = fb.bounding_box();
605            let scr_w = bounds.size.width as i32;
606            let scr_h = bounds.size.height as i32;
607            if p1.x < 0 && p2.x < 0 && p3.x < 0 {
608                return;
609            }
610            if p1.x >= scr_w && p2.x >= scr_w && p3.x >= scr_w {
611                return;
612            }
613            if p1.y < 0 && p2.y < 0 && p3.y < 0 {
614                return;
615            }
616            if p1.y >= scr_h && p2.y >= scr_h && p3.y >= scr_h {
617                return;
618            }
619
620            if p2.y == p3.y {
621                fill_bottom_flat_gouraud(p1, p2, p3, c1, c2, c3, fb);
622            } else if p1.y == p2.y {
623                fill_top_flat_gouraud(p1, p2, p3, c1, c2, c3, fb);
624            } else {
625                // Split triangle into two flat triangles
626                let t = (p2.y - p1.y) as f32 / (p3.y - p1.y) as f32;
627                let p4 = Point::new((p1.x as f32 + t * (p3.x - p1.x) as f32) as i32, p2.y);
628                // Interpolate color at split point
629                let c4 = interpolate_color(c1, c3, t);
630
631                fill_bottom_flat_gouraud(p1, p2, p4, c1, c2, c4, fb);
632                fill_top_flat_gouraud(p2, p4, p3, c2, c4, c3, fb);
633            }
634        }
635        DrawPrimitive::GouraudTriangleWithDepth {
636            points,
637            depths: _,
638            colors,
639        } => {
640            // This variant should use draw_zbuffered() instead
641            // For compatibility, render without Z-buffering (ignoring depths)
642            let prim = DrawPrimitive::GouraudTriangle { points, colors };
643            draw(prim, fb);
644        }
645        DrawPrimitive::TexturedTriangle { .. }
646        | DrawPrimitive::TexturedTriangleWithDepth { .. } => {
647            // Textured triangles require a texture manager
648            // Use draw_zbuffered_with_textures() instead
649            // Cannot render without texture manager, so this is a no-op
650        }
651    }
652}
653
654// Interpolate between two colors
655#[inline]
656fn interpolate_color(
657    c1: embedded_graphics_core::pixelcolor::Rgb565,
658    c2: embedded_graphics_core::pixelcolor::Rgb565,
659    t: f32,
660) -> embedded_graphics_core::pixelcolor::Rgb565 {
661    let r1 = c1.r() as f32;
662    let g1 = c1.g() as f32;
663    let b1 = c1.b() as f32;
664
665    let r2 = c2.r() as f32;
666    let g2 = c2.g() as f32;
667    let b2 = c2.b() as f32;
668
669    let r = (r1 + t * (r2 - r1)) as u8;
670    let g = (g1 + t * (g2 - g1)) as u8;
671    let b = (b1 + t * (b2 - b1)) as u8;
672
673    embedded_graphics_core::pixelcolor::Rgb565::new(r, g, b)
674}
675
676// Gouraud shading - bottom flat triangle with color interpolation
677fn fill_bottom_flat_gouraud<D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>>(
678    p1: Point,
679    p2: Point,
680    p3: Point,
681    c1: embedded_graphics_core::pixelcolor::Rgb565,
682    c2: embedded_graphics_core::pixelcolor::Rgb565,
683    c3: embedded_graphics_core::pixelcolor::Rgb565,
684    fb: &mut D,
685) where
686    <D as DrawTarget>::Error: Debug,
687{
688    let height = (p2.y - p1.y) as f32;
689    if height == 0.0 {
690        return;
691    }
692
693    let mut edge1 = EdgeStepper::new(p1, p2, p1.y);
694    let mut edge2 = EdgeStepper::new(p1, p3, p1.y);
695
696    for scanline_y in p1.y..=p2.y {
697        let t = (scanline_y - p1.y) as f32 / height;
698        let color_left = interpolate_color(c1, c2, t);
699        let color_right = interpolate_color(c1, c3, t);
700
701        draw_horizontal_line_gouraud(
702            Point::new(edge1.current_x(), scanline_y),
703            Point::new(edge2.current_x(), scanline_y),
704            color_left,
705            color_right,
706            fb,
707        );
708
709        edge1.advance();
710        edge2.advance();
711    }
712}
713
714// Gouraud shading - top flat triangle with color interpolation
715fn fill_top_flat_gouraud<D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>>(
716    p1: Point,
717    p2: Point,
718    p3: Point,
719    c1: embedded_graphics_core::pixelcolor::Rgb565,
720    c2: embedded_graphics_core::pixelcolor::Rgb565,
721    c3: embedded_graphics_core::pixelcolor::Rgb565,
722    fb: &mut D,
723) where
724    <D as DrawTarget>::Error: Debug,
725{
726    // p1.y == p2.y (top flat), p3 is the bottom vertex; iterate top-down.
727    let height = (p3.y - p1.y) as f32;
728    if height == 0.0 {
729        return;
730    }
731
732    let mut edge1 = EdgeStepper::new(p1, p3, p1.y);
733    let mut edge2 = EdgeStepper::new(p2, p3, p1.y);
734
735    for scanline_y in p1.y..=p3.y {
736        let t = (scanline_y - p1.y) as f32 / height;
737        let color_left = interpolate_color(c1, c3, t);
738        let color_right = interpolate_color(c2, c3, t);
739
740        draw_horizontal_line_gouraud(
741            Point::new(edge1.current_x(), scanline_y),
742            Point::new(edge2.current_x(), scanline_y),
743            color_left,
744            color_right,
745            fb,
746        );
747
748        edge1.advance();
749        edge2.advance();
750    }
751}
752
753// Draw a horizontal line with color interpolation (Gouraud)
754fn draw_horizontal_line_gouraud<D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>>(
755    p1: Point,
756    p2: Point,
757    color1: embedded_graphics_core::pixelcolor::Rgb565,
758    color2: embedded_graphics_core::pixelcolor::Rgb565,
759    fb: &mut D,
760) where
761    <D as DrawTarget>::Error: Debug,
762{
763    let start = p1.x.min(p2.x);
764    let end = p1.x.max(p2.x);
765    let width = (end - start) as f32;
766
767    if width == 0.0 {
768        fb.draw_iter([embedded_graphics_core::Pixel(
769            Point::new(start, p1.y),
770            color1,
771        )])
772        .unwrap();
773        return;
774    }
775
776    for x in start..=end {
777        let t = (x - start) as f32 / width;
778        let color = interpolate_color(color1, color2, t);
779        fb.draw_iter([embedded_graphics_core::Pixel(Point::new(x, p1.y), color)])
780            .unwrap();
781    }
782}
783
784// Z-buffered drawing function
785// Using u32 for Z-buffer is much faster on embedded systems without FPU
786#[inline]
787pub fn draw_zbuffered<D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>>(
788    primitive: DrawPrimitive,
789    fb: &mut D,
790    zbuffer: &mut [u32],
791    width: usize,
792) where
793    <D as DrawTarget>::Error: Debug,
794{
795    // Call with no effects for backward compatibility
796    draw_zbuffered_with_effects(primitive, fb, zbuffer, width, None, None);
797}
798
799/// Z-buffered drawing with analytical edge anti-aliasing.
800///
801/// Renders triangles with sub-pixel-accurate left/right edge coverage by
802/// blending the boundary pixels with the existing framebuffer contents.
803/// Inner pixels of each scanline use the same fast path as `draw_zbuffered`.
804/// Lines use Wu's algorithm.
805///
806/// Requires `ReadPixel` on the framebuffer for the boundary blends.
807#[cfg(feature = "aa-heuristic")]
808#[inline]
809pub fn draw_zbuffered_aa<D>(primitive: DrawPrimitive, fb: &mut D, zbuffer: &mut [u32], width: usize)
810where
811    D: DrawTarget<Color = Rgb565> + ReadPixel,
812    <D as DrawTarget>::Error: Debug,
813{
814    match primitive {
815        DrawPrimitive::ColoredTriangleWithDepth {
816            mut points,
817            mut depths,
818            color,
819        } => {
820            if points[0].y > points[1].y {
821                points.swap(0, 1);
822                depths.swap(0, 1);
823            }
824            if points[0].y > points[2].y {
825                points.swap(0, 2);
826                depths.swap(0, 2);
827            }
828            if points[1].y > points[2].y {
829                points.swap(1, 2);
830                depths.swap(1, 2);
831            }
832            let [p1, p2, p3] = points;
833            let [z1, z2, z3] = depths;
834
835            // Off-screen culling.
836            let scr_w = width as i32;
837            let scr_h = (zbuffer.len() / width) as i32;
838            if p1.x < 0 && p2.x < 0 && p3.x < 0 {
839                return;
840            }
841            if p1.x >= scr_w && p2.x >= scr_w && p3.x >= scr_w {
842                return;
843            }
844            if p1.y < 0 && p2.y < 0 && p3.y < 0 {
845                return;
846            }
847            if p1.y >= scr_h && p2.y >= scr_h && p3.y >= scr_h {
848                return;
849            }
850
851            fill_triangle_zbuffered_aa(p1, p2, p3, z1, z2, z3, color, fb, zbuffer, width);
852        }
853        DrawPrimitive::Line([p1, p2], color) => {
854            draw_line_aa(p1.x, p1.y, p2.x, p2.y, color, fb);
855        }
856        // Anything else (Gouraud, textured, points) — fall back to the
857        // non-AA path. AA variants for those can be added as needed.
858        other => draw_zbuffered(other, fb, zbuffer, width),
859    }
860}
861
862#[cfg(feature = "aa-heuristic")]
863#[inline(always)]
864fn fill_triangle_zbuffered_aa<D>(
865    p1: nalgebra::Point2<i32>,
866    p2: nalgebra::Point2<i32>,
867    p3: nalgebra::Point2<i32>,
868    z1: f32,
869    z2: f32,
870    z3: f32,
871    color: Rgb565,
872    fb: &mut D,
873    zbuffer: &mut [u32],
874    width: usize,
875) where
876    D: DrawTarget<Color = Rgb565> + ReadPixel,
877    <D as DrawTarget>::Error: Debug,
878{
879    let p1_eg = Point::new(p1.x, p1.y);
880    let p2_eg = Point::new(p2.x, p2.y);
881    let p3_eg = Point::new(p3.x, p3.y);
882
883    let z1_int = (z1 * 65536.0) as u32;
884    let z2_int = (z2 * 65536.0) as u32;
885    let z3_int = (z3 * 65536.0) as u32;
886
887    if p2_eg.y == p3_eg.y {
888        fill_bottom_flat_aa(
889            p1_eg, p2_eg, p3_eg, z1_int, z2_int, z3_int, color, fb, zbuffer, width,
890        );
891    } else if p1_eg.y == p2_eg.y {
892        fill_top_flat_aa(
893            p1_eg, p2_eg, p3_eg, z1_int, z2_int, z3_int, color, fb, zbuffer, width,
894        );
895    } else {
896        let t = (p2_eg.y - p1_eg.y) as f32 / (p3_eg.y - p1_eg.y) as f32;
897        let p4 = Point::new(
898            (p1_eg.x as f32 + t * (p3_eg.x - p1_eg.x) as f32) as i32,
899            p2_eg.y,
900        );
901        let z4_int = (z1_int as i64 + (t * (z3_int as i64 - z1_int as i64) as f32) as i64) as u32;
902        fill_bottom_flat_aa(
903            p1_eg, p2_eg, p4, z1_int, z2_int, z4_int, color, fb, zbuffer, width,
904        );
905        fill_top_flat_aa(
906            p2_eg, p4, p3_eg, z2_int, z4_int, z3_int, color, fb, zbuffer, width,
907        );
908    }
909}
910
911#[cfg(feature = "aa-heuristic")]
912#[inline(always)]
913fn fill_bottom_flat_aa<D>(
914    p1: Point,
915    p2: Point,
916    p3: Point,
917    z1: u32,
918    z2: u32,
919    z3: u32,
920    color: Rgb565,
921    fb: &mut D,
922    zbuffer: &mut [u32],
923    width: usize,
924) where
925    D: DrawTarget<Color = Rgb565> + ReadPixel,
926    <D as DrawTarget>::Error: Debug,
927{
928    let height = p2.y - p1.y;
929    if height == 0 {
930        return;
931    }
932    let invslope1 = ((p2.x - p1.x) << 16) / height;
933    let invslope2 = ((p3.x - p1.x) << 16) / height;
934
935    let mut curx1 = p1.x << 16;
936    let mut curx2 = p1.x << 16;
937
938    for scanline_y in p1.y..=p2.y {
939        let dy = scanline_y - p1.y;
940        let z_left = (z1 as i64 + ((z2 as i64 - z1 as i64) * dy as i64 / height as i64)) as u32;
941        let z_right = (z1 as i64 + ((z3 as i64 - z1 as i64) * dy as i64 / height as i64)) as u32;
942
943        aa_scanline(
944            curx1, curx2, scanline_y, z_left, z_right, color, fb, zbuffer, width,
945        );
946
947        curx1 += invslope1;
948        curx2 += invslope2;
949    }
950}
951
952#[cfg(feature = "aa-heuristic")]
953#[inline(always)]
954fn fill_top_flat_aa<D>(
955    p1: Point,
956    p2: Point,
957    p3: Point,
958    z1: u32,
959    z2: u32,
960    z3: u32,
961    color: Rgb565,
962    fb: &mut D,
963    zbuffer: &mut [u32],
964    width: usize,
965) where
966    D: DrawTarget<Color = Rgb565> + ReadPixel,
967    <D as DrawTarget>::Error: Debug,
968{
969    let height = p3.y - p1.y;
970    if height == 0 {
971        return;
972    }
973    let invslope1 = ((p3.x - p1.x) << 16) / height;
974    let invslope2 = ((p3.x - p2.x) << 16) / height;
975
976    let mut curx1 = p3.x << 16;
977    let mut curx2 = p3.x << 16;
978
979    for scanline_y in (p1.y..=p3.y).rev() {
980        let dy = scanline_y - p1.y;
981        let z_left = (z1 as i64 + ((z3 as i64 - z1 as i64) * dy as i64 / height as i64)) as u32;
982        let z_right = (z2 as i64 + ((z3 as i64 - z2 as i64) * dy as i64 / height as i64)) as u32;
983
984        aa_scanline(
985            curx1, curx2, scanline_y, z_left, z_right, color, fb, zbuffer, width,
986        );
987
988        curx1 -= invslope1;
989        curx2 -= invslope2;
990    }
991}
992
993/// Render one scanline with analytical left/right edge coverage.
994///
995/// `cx1` / `cx2` are 16.16 fixed-point edge positions. The fractional parts
996/// give us per-edge sub-pixel coverage; the integer span between them is
997/// rendered with the existing fully-opaque fast path.
998#[cfg(feature = "aa-heuristic")]
999#[inline(always)]
1000fn aa_scanline<D>(
1001    cx1: i32,
1002    cx2: i32,
1003    y: i32,
1004    z_left: u32,
1005    z_right: u32,
1006    color: Rgb565,
1007    fb: &mut D,
1008    zbuffer: &mut [u32],
1009    width: usize,
1010) where
1011    D: DrawTarget<Color = Rgb565> + ReadPixel,
1012    <D as DrawTarget>::Error: Debug,
1013{
1014    // Normalize: left should be the smaller fixed-point x.
1015    let (left_fx, right_fx, z_l, z_r) = if cx1 <= cx2 {
1016        (cx1, cx2, z_left, z_right)
1017    } else {
1018        (cx2, cx1, z_right, z_left)
1019    };
1020
1021    let l_int = left_fx >> 16;
1022    let r_int = right_fx >> 16;
1023    let l_frac_q16 = (left_fx & 0xFFFF) as u32;
1024    let r_frac_q16 = (right_fx & 0xFFFF) as u32;
1025
1026    // Linear z interpolation across the inner span.
1027    let span = r_int - l_int;
1028
1029    if l_int == r_int {
1030        // Sub-pixel span: triangle width < 1px on this row. Coverage equals
1031        // the float-difference of the edge positions.
1032        let cov_q16 = r_frac_q16.saturating_sub(l_frac_q16);
1033        aa_pixel(fb, l_int, y, color, z_l, zbuffer, width, cov_q16 >> 8);
1034        return;
1035    }
1036
1037    // Left boundary: covered fraction is (1 - l_frac).
1038    let left_cov_q8 = 256 - (l_frac_q16 >> 8);
1039    aa_pixel(fb, l_int, y, color, z_l, zbuffer, width, left_cov_q8);
1040
1041    // Inner pixels: full coverage. Reuse the existing scanline fast path
1042    // semantics inline (z-test + write, no read-blend).
1043    if span > 1 {
1044        for x in (l_int + 1)..r_int {
1045            if x < 0 {
1046                continue;
1047            }
1048            let idx = y as usize * width + x as usize;
1049            if idx >= zbuffer.len() {
1050                continue;
1051            }
1052            // Linear interp z across the inner pixels
1053            let t_num = (x - l_int) as i64;
1054            let t_den = span as i64;
1055            let z = (z_l as i64 + ((z_r as i64 - z_l as i64) * t_num / t_den)) as u32;
1056            if z < zbuffer[idx].saturating_add(DEPTH_EPSILON) {
1057                zbuffer[idx] = z;
1058                fb.draw_iter([embedded_graphics_core::Pixel(Point::new(x, y), color)])
1059                    .unwrap();
1060            }
1061        }
1062    }
1063
1064    // Right boundary: covered fraction is r_frac itself.
1065    if r_frac_q16 > 0 {
1066        let right_cov_q8 = r_frac_q16 >> 8;
1067        aa_pixel(fb, r_int, y, color, z_r, zbuffer, width, right_cov_q8);
1068    }
1069}
1070
1071/// Z-buffered drawing with coverage-tracked analytical edge AA.
1072///
1073/// Differs from `draw_zbuffered_aa` by maintaining a per-pixel coverage
1074/// buffer (`u8`, 0..255) so that multiple triangles meeting at a shared
1075/// edge composite additively rather than overwriting each other. This
1076/// eliminates the residual color-step seam at coplanar shared edges that
1077/// the simpler `draw_zbuffered_aa` heuristic leaves behind.
1078///
1079/// **Caller protocol per frame:**
1080/// 1. Clear `zbuffer` to `u32::MAX` and `coverage` to `0`.
1081/// 2. Render all primitives via this function.
1082/// 3. Call [`composite_aa_background`] with the desired background color.
1083///    This blends `bg` into pixels that aren't fully covered.
1084///
1085/// **Cost:** an additional `width * height` byte buffer (~6 KiB at 96×64).
1086/// Inner-pixel rasterization is the same fast path as `draw_zbuffered`.
1087#[cfg(feature = "aa-coverage")]
1088#[inline]
1089pub fn draw_zbuffered_aa_coverage<D>(
1090    primitive: DrawPrimitive,
1091    fb: &mut D,
1092    zbuffer: &mut [u32],
1093    coverage: &mut [u8],
1094    width: usize,
1095) where
1096    D: DrawTarget<Color = Rgb565> + ReadPixel,
1097    <D as DrawTarget>::Error: Debug,
1098{
1099    match primitive {
1100        DrawPrimitive::ColoredTriangleWithDepth {
1101            mut points,
1102            mut depths,
1103            color,
1104        } => {
1105            if points[0].y > points[1].y {
1106                points.swap(0, 1);
1107                depths.swap(0, 1);
1108            }
1109            if points[0].y > points[2].y {
1110                points.swap(0, 2);
1111                depths.swap(0, 2);
1112            }
1113            if points[1].y > points[2].y {
1114                points.swap(1, 2);
1115                depths.swap(1, 2);
1116            }
1117            let [p1, p2, p3] = points;
1118            let [z1, z2, z3] = depths;
1119            fill_triangle_zbuffered_aa_cov(
1120                p1, p2, p3, z1, z2, z3, color, fb, zbuffer, coverage, width,
1121            );
1122        }
1123        DrawPrimitive::Line([p1, p2], color) => {
1124            // Use the coverage-aware Wu's variant so the bg composite at
1125            // end-of-frame doesn't overwrite line pixels.
1126            draw_line_aa_coverage(p1.x, p1.y, p2.x, p2.y, color, fb, coverage, width);
1127        }
1128        other => draw_zbuffered(other, fb, zbuffer, width),
1129    }
1130}
1131
1132/// Wu's anti-aliased line that updates a coverage buffer alongside the
1133/// framebuffer write. Use with `draw_zbuffered_aa_coverage` so the bg
1134/// composite at end-of-frame respects line pixels.
1135#[cfg(feature = "aa-coverage")]
1136pub fn draw_line_aa_coverage<D>(
1137    x0: i32,
1138    y0: i32,
1139    x1: i32,
1140    y1: i32,
1141    color: Rgb565,
1142    fb: &mut D,
1143    coverage: &mut [u8],
1144    width: usize,
1145) where
1146    D: DrawTarget<Color = Rgb565> + ReadPixel,
1147    <D as DrawTarget>::Error: Debug,
1148{
1149    let dx = (x1 - x0).abs();
1150    let dy = (y1 - y0).abs();
1151    let steep = dy > dx;
1152    let (x0, y0, x1, y1) = if steep {
1153        (y0, x0, y1, x1)
1154    } else {
1155        (x0, y0, x1, y1)
1156    };
1157    let (x0, y0, x1, y1) = if x0 > x1 {
1158        (x1, y1, x0, y0)
1159    } else {
1160        (x0, y0, x1, y1)
1161    };
1162    let dx = x1 - x0;
1163    let dy = y1 - y0;
1164    if dx == 0 {
1165        let (px, py) = if steep { (y0, x0) } else { (x0, y0) };
1166        plot_aa_cov(fb, px, py, color, coverage, width, 256);
1167        return;
1168    }
1169    let gradient: i32 = ((dy as i64) << 16) as i32 / dx;
1170    let mut intery: i32 = y0 << 16;
1171    for x in x0..=x1 {
1172        let y_int = intery >> 16;
1173        let frac_q16 = (intery & 0xFFFF) as u32;
1174        let cov_top = 256 - (frac_q16 >> 8);
1175        let cov_bot = frac_q16 >> 8;
1176        if steep {
1177            plot_aa_cov(fb, y_int, x, color, coverage, width, cov_top);
1178            plot_aa_cov(fb, y_int + 1, x, color, coverage, width, cov_bot);
1179        } else {
1180            plot_aa_cov(fb, x, y_int, color, coverage, width, cov_top);
1181            plot_aa_cov(fb, x, y_int + 1, color, coverage, width, cov_bot);
1182        }
1183        intery += gradient;
1184    }
1185}
1186
1187/// Coverage-aware single-pixel plot for Wu's lines. Mirrors the four-case
1188/// logic of `aa_pixel_cov` but without the z-test (lines don't carry depth).
1189#[cfg(feature = "aa-coverage")]
1190#[inline(always)]
1191fn plot_aa_cov<D>(
1192    fb: &mut D,
1193    x: i32,
1194    y: i32,
1195    color: Rgb565,
1196    coverage: &mut [u8],
1197    width: usize,
1198    coverage_q8: u32,
1199) where
1200    D: DrawTarget<Color = Rgb565> + ReadPixel,
1201    <D as DrawTarget>::Error: Debug,
1202{
1203    if x < 0 || y < 0 || coverage_q8 == 0 {
1204        return;
1205    }
1206    let idx = y as usize * width + x as usize;
1207    if idx >= coverage.len() {
1208        return;
1209    }
1210    let p = Point::new(x, y);
1211
1212    if coverage_q8 >= 256 {
1213        coverage[idx] = 255;
1214        fb.draw_iter([embedded_graphics_core::Pixel(p, color)])
1215            .unwrap();
1216        return;
1217    }
1218
1219    let prev_cov = coverage[idx] as u32;
1220
1221    if prev_cov == 0 {
1222        let claim_255 = (coverage_q8 * 255) >> 8;
1223        coverage[idx] = claim_255 as u8;
1224        fb.draw_iter([embedded_graphics_core::Pixel(p, color)])
1225            .unwrap();
1226        return;
1227    }
1228
1229    if prev_cov >= 255 {
1230        let existing = fb.read_pixel(p);
1231        let result = blend_q8(existing, color, coverage_q8);
1232        fb.draw_iter([embedded_graphics_core::Pixel(p, result)])
1233            .unwrap();
1234        return;
1235    }
1236
1237    let remaining = 255 - prev_cov;
1238    let claim_255 = ((coverage_q8 * 255) >> 8).min(remaining);
1239    if claim_255 == 0 {
1240        return;
1241    }
1242    let new_total = prev_cov + claim_255;
1243    let existing = fb.read_pixel(p);
1244    let blend_factor = (claim_255 * 256) / new_total;
1245    let result = blend_q8(existing, color, blend_factor);
1246    coverage[idx] = new_total as u8;
1247    fb.draw_iter([embedded_graphics_core::Pixel(p, result)])
1248        .unwrap();
1249}
1250
1251/// Composite background color into pixels that weren't fully covered by
1252/// the AA rasterizer. Run once per frame after all primitives have been
1253/// drawn via `draw_zbuffered_aa_coverage`.
1254#[cfg(feature = "aa-coverage")]
1255pub fn composite_aa_background<D>(
1256    fb: &mut D,
1257    coverage: &[u8],
1258    bg: Rgb565,
1259    width: usize,
1260    height: usize,
1261) where
1262    D: DrawTarget<Color = Rgb565> + ReadPixel,
1263    <D as DrawTarget>::Error: Debug,
1264{
1265    for y in 0..height {
1266        for x in 0..width {
1267            let idx = y * width + x;
1268            let cov = coverage[idx];
1269            if cov == 255 {
1270                continue; // pixel fully owned by triangles, nothing to do
1271            }
1272            let p = Point::new(x as i32, y as i32);
1273            let final_color = if cov == 0 {
1274                bg
1275            } else {
1276                // Pixel holds the (already pre-composited) accumulated
1277                // triangle color, weighted by `cov / 255`. Composite the
1278                // remaining `(255 - cov) / 255` with the bg.
1279                let tri_color = fb.read_pixel(p);
1280                // Convert 0..255 to 0..256 q8 coverage for blend_q8.
1281                let cov_q8 = ((cov as u32) * 256) / 255;
1282                blend_q8(bg, tri_color, cov_q8)
1283            };
1284            fb.draw_iter([embedded_graphics_core::Pixel(p, final_color)])
1285                .unwrap();
1286        }
1287    }
1288}
1289
1290/// Z-test + coverage-tracked write of a single AA pixel.
1291///
1292/// Four cases, branched on `coverage_q8` (this triangle's pixel coverage)
1293/// and `coverage[idx]` (sum of prior triangles' coverage at this pixel):
1294///
1295/// 1. Full coverage (`coverage_q8 >= 256`): triangle covers the pixel
1296///    completely. Overwrite. Coverage saturates to 255.
1297/// 2. Virgin pixel + partial: store pure triangle color; bg gets composited
1298///    by `composite_aa_background` at end-of-frame using the claimed cov.
1299/// 3. Already-fully-covered pixel + partial closer triangle: blend the new
1300///    color over the existing (existing acts as the local "background"
1301///    since it's the visible scene behind the new triangle).
1302/// 4. Partially-claimed pixel + partial new: weighted-average accumulation
1303///    of pure triangle colors. This is the shared-coplanar-edge case.
1304#[cfg(feature = "aa-coverage")]
1305#[inline(always)]
1306fn aa_pixel_cov<D>(
1307    fb: &mut D,
1308    x: i32,
1309    y: i32,
1310    color: Rgb565,
1311    z: u32,
1312    zbuffer: &mut [u32],
1313    coverage: &mut [u8],
1314    width: usize,
1315    coverage_q8: u32,
1316) where
1317    D: DrawTarget<Color = Rgb565> + ReadPixel,
1318    <D as DrawTarget>::Error: Debug,
1319{
1320    if x < 0 || y < 0 || coverage_q8 == 0 {
1321        return;
1322    }
1323    let idx = y as usize * width + x as usize;
1324    if idx >= zbuffer.len() {
1325        return;
1326    }
1327    if z >= zbuffer[idx].saturating_add(DEPTH_EPSILON) {
1328        return;
1329    }
1330
1331    let p = Point::new(x, y);
1332
1333    // Case 1: full coverage — overwrite unconditionally.
1334    if coverage_q8 >= 256 {
1335        coverage[idx] = 255;
1336        zbuffer[idx] = z;
1337        fb.draw_iter([embedded_graphics_core::Pixel(p, color)])
1338            .unwrap();
1339        return;
1340    }
1341
1342    let prev_cov = coverage[idx] as u32;
1343
1344    if prev_cov == 0 {
1345        // Case 2: virgin pixel + partial coverage. Pure triangle color;
1346        // bg composite at end of frame fills the unclaimed remainder.
1347        let claim_255 = (coverage_q8 * 255) >> 8;
1348        coverage[idx] = claim_255 as u8;
1349        zbuffer[idx] = z;
1350        fb.draw_iter([embedded_graphics_core::Pixel(p, color)])
1351            .unwrap();
1352        return;
1353    }
1354
1355    if prev_cov >= 255 {
1356        // Case 3: pixel was fully claimed by farther geometry. We're closer
1357        // (z-test passed). Anti-alias the new triangle's edge against the
1358        // existing pixel as if it were the local background. Total coverage
1359        // stays at 255 — no bg composite needed.
1360        let existing = fb.read_pixel(p);
1361        let result = blend_q8(existing, color, coverage_q8);
1362        zbuffer[idx] = z;
1363        fb.draw_iter([embedded_graphics_core::Pixel(p, result)])
1364            .unwrap();
1365        return;
1366    }
1367
1368    // Case 4: partially-claimed pixel + partial new triangle. Weighted-
1369    // average accumulation. This is the coplanar-shared-edge case.
1370    let remaining = 255 - prev_cov;
1371    let claim_255 = ((coverage_q8 * 255) >> 8).min(remaining);
1372    if claim_255 == 0 {
1373        return;
1374    }
1375    let new_total = prev_cov + claim_255;
1376    let existing = fb.read_pixel(p);
1377    let blend_factor = (claim_255 * 256) / new_total;
1378    let result = blend_q8(existing, color, blend_factor);
1379    coverage[idx] = new_total as u8;
1380    zbuffer[idx] = z;
1381    fb.draw_iter([embedded_graphics_core::Pixel(p, result)])
1382        .unwrap();
1383}
1384
1385#[cfg(feature = "aa-coverage")]
1386#[inline(always)]
1387fn fill_triangle_zbuffered_aa_cov<D>(
1388    p1: nalgebra::Point2<i32>,
1389    p2: nalgebra::Point2<i32>,
1390    p3: nalgebra::Point2<i32>,
1391    z1: f32,
1392    z2: f32,
1393    z3: f32,
1394    color: Rgb565,
1395    fb: &mut D,
1396    zbuffer: &mut [u32],
1397    coverage: &mut [u8],
1398    width: usize,
1399) where
1400    D: DrawTarget<Color = Rgb565> + ReadPixel,
1401    <D as DrawTarget>::Error: Debug,
1402{
1403    let p1_eg = Point::new(p1.x, p1.y);
1404    let p2_eg = Point::new(p2.x, p2.y);
1405    let p3_eg = Point::new(p3.x, p3.y);
1406
1407    let z1_int = (z1 * 65536.0) as u32;
1408    let z2_int = (z2 * 65536.0) as u32;
1409    let z3_int = (z3 * 65536.0) as u32;
1410
1411    if p2_eg.y == p3_eg.y {
1412        fill_bottom_flat_aa_cov(
1413            p1_eg, p2_eg, p3_eg, z1_int, z2_int, z3_int, color, fb, zbuffer, coverage, width,
1414        );
1415    } else if p1_eg.y == p2_eg.y {
1416        fill_top_flat_aa_cov(
1417            p1_eg, p2_eg, p3_eg, z1_int, z2_int, z3_int, color, fb, zbuffer, coverage, width,
1418        );
1419    } else {
1420        let t = (p2_eg.y - p1_eg.y) as f32 / (p3_eg.y - p1_eg.y) as f32;
1421        let p4 = Point::new(
1422            (p1_eg.x as f32 + t * (p3_eg.x - p1_eg.x) as f32) as i32,
1423            p2_eg.y,
1424        );
1425        let z4_int = (z1_int as i64 + (t * (z3_int as i64 - z1_int as i64) as f32) as i64) as u32;
1426        fill_bottom_flat_aa_cov(
1427            p1_eg, p2_eg, p4, z1_int, z2_int, z4_int, color, fb, zbuffer, coverage, width,
1428        );
1429        fill_top_flat_aa_cov(
1430            p2_eg, p4, p3_eg, z2_int, z4_int, z3_int, color, fb, zbuffer, coverage, width,
1431        );
1432    }
1433}
1434
1435#[cfg(feature = "aa-coverage")]
1436#[inline(always)]
1437fn fill_bottom_flat_aa_cov<D>(
1438    p1: Point,
1439    p2: Point,
1440    p3: Point,
1441    z1: u32,
1442    z2: u32,
1443    z3: u32,
1444    color: Rgb565,
1445    fb: &mut D,
1446    zbuffer: &mut [u32],
1447    coverage: &mut [u8],
1448    width: usize,
1449) where
1450    D: DrawTarget<Color = Rgb565> + ReadPixel,
1451    <D as DrawTarget>::Error: Debug,
1452{
1453    let height = p2.y - p1.y;
1454    if height == 0 {
1455        return;
1456    }
1457    let invslope1 = ((p2.x - p1.x) << 16) / height;
1458    let invslope2 = ((p3.x - p1.x) << 16) / height;
1459
1460    let mut curx1 = p1.x << 16;
1461    let mut curx2 = p1.x << 16;
1462
1463    for scanline_y in p1.y..=p2.y {
1464        let dy = scanline_y - p1.y;
1465        let z_left = (z1 as i64 + ((z2 as i64 - z1 as i64) * dy as i64 / height as i64)) as u32;
1466        let z_right = (z1 as i64 + ((z3 as i64 - z1 as i64) * dy as i64 / height as i64)) as u32;
1467
1468        aa_scanline_cov(
1469            curx1, curx2, scanline_y, z_left, z_right, color, fb, zbuffer, coverage, width,
1470        );
1471
1472        curx1 += invslope1;
1473        curx2 += invslope2;
1474    }
1475}
1476
1477#[cfg(feature = "aa-coverage")]
1478#[inline(always)]
1479fn fill_top_flat_aa_cov<D>(
1480    p1: Point,
1481    p2: Point,
1482    p3: Point,
1483    z1: u32,
1484    z2: u32,
1485    z3: u32,
1486    color: Rgb565,
1487    fb: &mut D,
1488    zbuffer: &mut [u32],
1489    coverage: &mut [u8],
1490    width: usize,
1491) where
1492    D: DrawTarget<Color = Rgb565> + ReadPixel,
1493    <D as DrawTarget>::Error: Debug,
1494{
1495    let height = p3.y - p1.y;
1496    if height == 0 {
1497        return;
1498    }
1499    let invslope1 = ((p3.x - p1.x) << 16) / height;
1500    let invslope2 = ((p3.x - p2.x) << 16) / height;
1501
1502    let mut curx1 = p3.x << 16;
1503    let mut curx2 = p3.x << 16;
1504
1505    for scanline_y in (p1.y..=p3.y).rev() {
1506        let dy = scanline_y - p1.y;
1507        let z_left = (z1 as i64 + ((z3 as i64 - z1 as i64) * dy as i64 / height as i64)) as u32;
1508        let z_right = (z2 as i64 + ((z3 as i64 - z2 as i64) * dy as i64 / height as i64)) as u32;
1509
1510        aa_scanline_cov(
1511            curx1, curx2, scanline_y, z_left, z_right, color, fb, zbuffer, coverage, width,
1512        );
1513
1514        curx1 -= invslope1;
1515        curx2 -= invslope2;
1516    }
1517}
1518
1519#[cfg(feature = "aa-coverage")]
1520#[inline(always)]
1521fn aa_scanline_cov<D>(
1522    cx1: i32,
1523    cx2: i32,
1524    y: i32,
1525    z_left: u32,
1526    z_right: u32,
1527    color: Rgb565,
1528    fb: &mut D,
1529    zbuffer: &mut [u32],
1530    coverage: &mut [u8],
1531    width: usize,
1532) where
1533    D: DrawTarget<Color = Rgb565> + ReadPixel,
1534    <D as DrawTarget>::Error: Debug,
1535{
1536    let (left_fx, right_fx, z_l, z_r) = if cx1 <= cx2 {
1537        (cx1, cx2, z_left, z_right)
1538    } else {
1539        (cx2, cx1, z_right, z_left)
1540    };
1541
1542    let l_int = left_fx >> 16;
1543    let r_int = right_fx >> 16;
1544    let l_frac_q16 = (left_fx & 0xFFFF) as u32;
1545    let r_frac_q16 = (right_fx & 0xFFFF) as u32;
1546    let span = r_int - l_int;
1547
1548    if l_int == r_int {
1549        let cov_q16 = r_frac_q16.saturating_sub(l_frac_q16);
1550        aa_pixel_cov(
1551            fb,
1552            l_int,
1553            y,
1554            color,
1555            z_l,
1556            zbuffer,
1557            coverage,
1558            width,
1559            cov_q16 >> 8,
1560        );
1561        return;
1562    }
1563
1564    let left_cov_q8 = 256 - (l_frac_q16 >> 8);
1565    aa_pixel_cov(
1566        fb,
1567        l_int,
1568        y,
1569        color,
1570        z_l,
1571        zbuffer,
1572        coverage,
1573        width,
1574        left_cov_q8,
1575    );
1576
1577    if span > 1 {
1578        for x in (l_int + 1)..r_int {
1579            // Inner pixels are full coverage (q8 = 256). Use the same code
1580            // path as boundary pixels so coverage tracks correctly.
1581            let t_num = (x - l_int) as i64;
1582            let t_den = span as i64;
1583            let z = (z_l as i64 + ((z_r as i64 - z_l as i64) * t_num / t_den)) as u32;
1584            aa_pixel_cov(fb, x, y, color, z, zbuffer, coverage, width, 256);
1585        }
1586    }
1587
1588    if r_frac_q16 > 0 {
1589        let right_cov_q8 = r_frac_q16 >> 8;
1590        aa_pixel_cov(
1591            fb,
1592            r_int,
1593            y,
1594            color,
1595            z_r,
1596            zbuffer,
1597            coverage,
1598            width,
1599            right_cov_q8,
1600        );
1601    }
1602}
1603
1604/// Wu's anti-aliased line algorithm.
1605///
1606/// Walks the major axis one integer step at a time; at each step writes two
1607/// pixels straddling the line with complementary fractional coverage.
1608#[cfg(feature = "aa")]
1609pub fn draw_line_aa<D>(x0: i32, y0: i32, x1: i32, y1: i32, color: Rgb565, fb: &mut D)
1610where
1611    D: DrawTarget<Color = Rgb565> + ReadPixel,
1612    <D as DrawTarget>::Error: Debug,
1613{
1614    let dx = (x1 - x0).abs();
1615    let dy = (y1 - y0).abs();
1616    let steep = dy > dx;
1617    let (x0, y0, x1, y1) = if steep {
1618        (y0, x0, y1, x1)
1619    } else {
1620        (x0, y0, x1, y1)
1621    };
1622    let (x0, y0, x1, y1) = if x0 > x1 {
1623        (x1, y1, x0, y0)
1624    } else {
1625        (x0, y0, x1, y1)
1626    };
1627    let dx = x1 - x0;
1628    let dy = y1 - y0;
1629    if dx == 0 {
1630        // Single pixel
1631        let (px, py) = if steep { (y0, x0) } else { (x0, y0) };
1632        plot_aa(fb, px, py, color, 256);
1633        return;
1634    }
1635    // 16.16 fixed-point gradient
1636    let gradient: i32 = ((dy as i64) << 16) as i32 / dx;
1637    // Start at exact (x0, y0); intery accumulates the y position in 16.16.
1638    let mut intery: i32 = y0 << 16;
1639    for x in x0..=x1 {
1640        let y_int = intery >> 16;
1641        let frac_q16 = (intery & 0xFFFF) as u32;
1642        let cov_top = 256 - (frac_q16 >> 8); // pixel at y_int
1643        let cov_bot = frac_q16 >> 8; //         pixel at y_int + 1
1644        if steep {
1645            plot_aa(fb, y_int, x, color, cov_top);
1646            plot_aa(fb, y_int + 1, x, color, cov_bot);
1647        } else {
1648            plot_aa(fb, x, y_int, color, cov_top);
1649            plot_aa(fb, x, y_int + 1, color, cov_bot);
1650        }
1651        intery += gradient;
1652    }
1653}
1654
1655#[cfg(feature = "aa")]
1656#[inline(always)]
1657fn plot_aa<D>(fb: &mut D, x: i32, y: i32, color: Rgb565, coverage_q8: u32)
1658where
1659    D: DrawTarget<Color = Rgb565> + ReadPixel,
1660    <D as DrawTarget>::Error: Debug,
1661{
1662    if coverage_q8 == 0 {
1663        return;
1664    }
1665    let final_color = if coverage_q8 >= 256 {
1666        color
1667    } else {
1668        let bg = fb.read_pixel(Point::new(x, y));
1669        blend_q8(bg, color, coverage_q8)
1670    };
1671    fb.draw_iter([embedded_graphics_core::Pixel(Point::new(x, y), final_color)])
1672        .unwrap();
1673}
1674
1675// Z-buffered drawing function with optional fog and dithering effects
1676// Requires a TextureManager for textured primitives
1677#[inline]
1678pub fn draw_zbuffered_with_effects<
1679    D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>,
1680>(
1681    primitive: DrawPrimitive,
1682    fb: &mut D,
1683    zbuffer: &mut [u32],
1684    width: usize,
1685    fog_config: Option<&FogConfig>,
1686    dither_config: Option<&DitherConfig>,
1687) where
1688    <D as DrawTarget>::Error: Debug,
1689{
1690    match primitive {
1691        DrawPrimitive::ColoredTriangleWithDepth {
1692            mut points,
1693            mut depths,
1694            color,
1695        } => {
1696            // Sort vertices by y coordinate (and corresponding depths)
1697            if points[0].y > points[1].y {
1698                points.swap(0, 1);
1699                depths.swap(0, 1);
1700            }
1701            if points[0].y > points[2].y {
1702                points.swap(0, 2);
1703                depths.swap(0, 2);
1704            }
1705            if points[1].y > points[2].y {
1706                points.swap(1, 2);
1707                depths.swap(1, 2);
1708            }
1709
1710            let [p1, p2, p3] = points;
1711            let [z1, z2, z3] = depths;
1712
1713            // Off-screen culling.
1714            let scr_w = width as i32;
1715            let scr_h = (zbuffer.len() / width) as i32;
1716            if p1.x < 0 && p2.x < 0 && p3.x < 0 {
1717                return;
1718            }
1719            if p1.x >= scr_w && p2.x >= scr_w && p3.x >= scr_w {
1720                return;
1721            }
1722            if p1.y < 0 && p2.y < 0 && p3.y < 0 {
1723                return;
1724            }
1725            if p1.y >= scr_h && p2.y >= scr_h && p3.y >= scr_h {
1726                return;
1727            }
1728
1729            fill_triangle_zbuffered(
1730                p1,
1731                p2,
1732                p3,
1733                z1,
1734                z2,
1735                z3,
1736                color,
1737                fb,
1738                zbuffer,
1739                width,
1740                fog_config,
1741                dither_config,
1742            );
1743        }
1744        DrawPrimitive::GouraudTriangleWithDepth {
1745            mut points,
1746            mut depths,
1747            mut colors,
1748        } => {
1749            // Sort vertices by y coordinate (and corresponding depths and colors)
1750            if points[0].y > points[1].y {
1751                points.swap(0, 1);
1752                depths.swap(0, 1);
1753                colors.swap(0, 1);
1754            }
1755            if points[0].y > points[2].y {
1756                points.swap(0, 2);
1757                depths.swap(0, 2);
1758                colors.swap(0, 2);
1759            }
1760            if points[1].y > points[2].y {
1761                points.swap(1, 2);
1762                depths.swap(1, 2);
1763                colors.swap(1, 2);
1764            }
1765
1766            let [p1, p2, p3] = points;
1767            let [z1, z2, z3] = depths;
1768            let [c1, c2, c3] = colors;
1769
1770            // Off-screen culling.
1771            let scr_w = width as i32;
1772            let scr_h = (zbuffer.len() / width) as i32;
1773            if p1.x < 0 && p2.x < 0 && p3.x < 0 {
1774                return;
1775            }
1776            if p1.x >= scr_w && p2.x >= scr_w && p3.x >= scr_w {
1777                return;
1778            }
1779            if p1.y < 0 && p2.y < 0 && p3.y < 0 {
1780                return;
1781            }
1782            if p1.y >= scr_h && p2.y >= scr_h && p3.y >= scr_h {
1783                return;
1784            }
1785
1786            fill_triangle_zbuffered_gouraud(
1787                p1,
1788                p2,
1789                p3,
1790                z1,
1791                z2,
1792                z3,
1793                c1,
1794                c2,
1795                c3,
1796                fb,
1797                zbuffer,
1798                width,
1799                fog_config,
1800                dither_config,
1801            );
1802        }
1803        // Textured triangles require texture manager - fall back to regular drawing
1804        DrawPrimitive::TexturedTriangle { .. }
1805        | DrawPrimitive::TexturedTriangleWithDepth { .. } => {
1806            // These variants require a texture manager which isn't provided here
1807            // Use draw_zbuffered_with_textures() instead
1808        }
1809        // For other primitives, fall back to regular drawing
1810        _ => draw(primitive, fb),
1811    }
1812}
1813
1814// Z-buffered drawing function with textures, fog, and dithering effects
1815#[inline]
1816pub fn draw_zbuffered_with_textures<
1817    D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>,
1818    const N: usize,
1819>(
1820    primitive: DrawPrimitive,
1821    fb: &mut D,
1822    zbuffer: &mut [u32],
1823    width: usize,
1824    texture_manager: &crate::texture::TextureManager<N>,
1825    fog_config: Option<&FogConfig>,
1826    dither_config: Option<&DitherConfig>,
1827) where
1828    <D as DrawTarget>::Error: Debug,
1829{
1830    match primitive {
1831        DrawPrimitive::TexturedTriangleWithDepth {
1832            mut points,
1833            mut depths,
1834            mut ws,
1835            mut uvs,
1836            texture_id,
1837        } => {
1838            // Get texture from manager
1839            if let Some(texture) = texture_manager.get(texture_id) {
1840                // Sort vertices by y coordinate (and corresponding depths, ws, and UVs)
1841                if points[0].y > points[1].y {
1842                    points.swap(0, 1);
1843                    depths.swap(0, 1);
1844                    ws.swap(0, 1);
1845                    uvs.swap(0, 1);
1846                }
1847                if points[0].y > points[2].y {
1848                    points.swap(0, 2);
1849                    depths.swap(0, 2);
1850                    ws.swap(0, 2);
1851                    uvs.swap(0, 2);
1852                }
1853                if points[1].y > points[2].y {
1854                    points.swap(1, 2);
1855                    depths.swap(1, 2);
1856                    ws.swap(1, 2);
1857                    uvs.swap(1, 2);
1858                }
1859
1860                let [p1, p2, p3] = points;
1861                let [z1, z2, z3] = depths;
1862                let [w1, w2, w3] = ws;
1863                let [uv1, uv2, uv3] = uvs;
1864
1865                // Off-screen culling.
1866                let scr_w = width as i32;
1867                let scr_h = (zbuffer.len() / width) as i32;
1868                if p1.x < 0 && p2.x < 0 && p3.x < 0 {
1869                    return;
1870                }
1871                if p1.x >= scr_w && p2.x >= scr_w && p3.x >= scr_w {
1872                    return;
1873                }
1874                if p1.y < 0 && p2.y < 0 && p3.y < 0 {
1875                    return;
1876                }
1877                if p1.y >= scr_h && p2.y >= scr_h && p3.y >= scr_h {
1878                    return;
1879                }
1880
1881                fill_triangle_zbuffered_textured(
1882                    p1,
1883                    p2,
1884                    p3,
1885                    z1,
1886                    z2,
1887                    z3,
1888                    w1,
1889                    w2,
1890                    w3,
1891                    uv1,
1892                    uv2,
1893                    uv3,
1894                    texture,
1895                    fb,
1896                    zbuffer,
1897                    width,
1898                    fog_config,
1899                    dither_config,
1900                );
1901            }
1902        }
1903        // For other primitives, fall back to regular z-buffered drawing
1904        _ => draw_zbuffered_with_effects(primitive, fb, zbuffer, width, fog_config, dither_config),
1905    }
1906}
1907
1908#[inline(always)]
1909fn fill_triangle_zbuffered<D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>>(
1910    p1: nalgebra::Point2<i32>,
1911    p2: nalgebra::Point2<i32>,
1912    p3: nalgebra::Point2<i32>,
1913    z1: f32,
1914    z2: f32,
1915    z3: f32,
1916    color: embedded_graphics_core::pixelcolor::Rgb565,
1917    fb: &mut D,
1918    zbuffer: &mut [u32],
1919    width: usize,
1920    fog_config: Option<&FogConfig>,
1921    dither_config: Option<&DitherConfig>,
1922) where
1923    <D as DrawTarget>::Error: Debug,
1924{
1925    // Convert to embedded_graphics Points
1926    let p1_eg = Point::new(p1.x, p1.y);
1927    let p2_eg = Point::new(p2.x, p2.y);
1928    let p3_eg = Point::new(p3.x, p3.y);
1929
1930    // Convert float depths to fixed-point integers (16.16 format)
1931    // This avoids floating-point operations in the inner loop
1932    let z1_int = (z1 * 65536.0) as u32;
1933    let z2_int = (z2 * 65536.0) as u32;
1934    let z3_int = (z3 * 65536.0) as u32;
1935
1936    // Handle flat triangles
1937    if p2_eg.y == p3_eg.y {
1938        fill_bottom_flat_triangle_zbuffered(
1939            p1_eg,
1940            p2_eg,
1941            p3_eg,
1942            z1_int,
1943            z2_int,
1944            z3_int,
1945            color,
1946            fb,
1947            zbuffer,
1948            width,
1949            fog_config,
1950            dither_config,
1951        );
1952    } else if p1_eg.y == p2_eg.y {
1953        fill_top_flat_triangle_zbuffered(
1954            p1_eg,
1955            p2_eg,
1956            p3_eg,
1957            z1_int,
1958            z2_int,
1959            z3_int,
1960            color,
1961            fb,
1962            zbuffer,
1963            width,
1964            fog_config,
1965            dither_config,
1966        );
1967    } else {
1968        // Split into two flat triangles
1969        let t = (p2_eg.y - p1_eg.y) as f32 / (p3_eg.y - p1_eg.y) as f32;
1970        let p4 = Point::new(
1971            (p1_eg.x as f32 + t * (p3_eg.x - p1_eg.x) as f32) as i32,
1972            p2_eg.y,
1973        );
1974        let z4_int = (z1_int as i64 + (t * (z3_int as i64 - z1_int as i64) as f32) as i64) as u32;
1975
1976        fill_bottom_flat_triangle_zbuffered(
1977            p1_eg,
1978            p2_eg,
1979            p4,
1980            z1_int,
1981            z2_int,
1982            z4_int,
1983            color,
1984            fb,
1985            zbuffer,
1986            width,
1987            fog_config,
1988            dither_config,
1989        );
1990        fill_top_flat_triangle_zbuffered(
1991            p2_eg,
1992            p4,
1993            p3_eg,
1994            z2_int,
1995            z4_int,
1996            z3_int,
1997            color,
1998            fb,
1999            zbuffer,
2000            width,
2001            fog_config,
2002            dither_config,
2003        );
2004    }
2005}
2006
2007// Gouraud-shaded triangle with z-buffering
2008#[inline(always)]
2009fn fill_triangle_zbuffered_gouraud<
2010    D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>,
2011>(
2012    p1: nalgebra::Point2<i32>,
2013    p2: nalgebra::Point2<i32>,
2014    p3: nalgebra::Point2<i32>,
2015    z1: f32,
2016    z2: f32,
2017    z3: f32,
2018    c1: embedded_graphics_core::pixelcolor::Rgb565,
2019    c2: embedded_graphics_core::pixelcolor::Rgb565,
2020    c3: embedded_graphics_core::pixelcolor::Rgb565,
2021    fb: &mut D,
2022    zbuffer: &mut [u32],
2023    width: usize,
2024    fog_config: Option<&FogConfig>,
2025    dither_config: Option<&DitherConfig>,
2026) where
2027    <D as DrawTarget>::Error: Debug,
2028{
2029    // Convert to embedded_graphics Points
2030    let p1_eg = Point::new(p1.x, p1.y);
2031    let p2_eg = Point::new(p2.x, p2.y);
2032    let p3_eg = Point::new(p3.x, p3.y);
2033
2034    // Convert float depths to fixed-point integers (16.16 format)
2035    let z1_int = (z1 * 65536.0) as u32;
2036    let z2_int = (z2 * 65536.0) as u32;
2037    let z3_int = (z3 * 65536.0) as u32;
2038
2039    // Handle flat triangles
2040    if p2_eg.y == p3_eg.y {
2041        fill_bottom_flat_triangle_zbuffered_gouraud(
2042            p1_eg,
2043            p2_eg,
2044            p3_eg,
2045            z1_int,
2046            z2_int,
2047            z3_int,
2048            c1,
2049            c2,
2050            c3,
2051            fb,
2052            zbuffer,
2053            width,
2054            fog_config,
2055            dither_config,
2056        );
2057    } else if p1_eg.y == p2_eg.y {
2058        fill_top_flat_triangle_zbuffered_gouraud(
2059            p1_eg,
2060            p2_eg,
2061            p3_eg,
2062            z1_int,
2063            z2_int,
2064            z3_int,
2065            c1,
2066            c2,
2067            c3,
2068            fb,
2069            zbuffer,
2070            width,
2071            fog_config,
2072            dither_config,
2073        );
2074    } else {
2075        // Split into two flat triangles
2076        let t = (p2_eg.y - p1_eg.y) as f32 / (p3_eg.y - p1_eg.y) as f32;
2077        let p4 = Point::new(
2078            (p1_eg.x as f32 + t * (p3_eg.x - p1_eg.x) as f32) as i32,
2079            p2_eg.y,
2080        );
2081        let z4_int = (z1_int as i64 + (t * (z3_int as i64 - z1_int as i64) as f32) as i64) as u32;
2082        let c4 = interpolate_color(c1, c3, t);
2083
2084        fill_bottom_flat_triangle_zbuffered_gouraud(
2085            p1_eg,
2086            p2_eg,
2087            p4,
2088            z1_int,
2089            z2_int,
2090            z4_int,
2091            c1,
2092            c2,
2093            c4,
2094            fb,
2095            zbuffer,
2096            width,
2097            fog_config,
2098            dither_config,
2099        );
2100        fill_top_flat_triangle_zbuffered_gouraud(
2101            p2_eg,
2102            p4,
2103            p3_eg,
2104            z2_int,
2105            z4_int,
2106            z3_int,
2107            c2,
2108            c4,
2109            c3,
2110            fb,
2111            zbuffer,
2112            width,
2113            fog_config,
2114            dither_config,
2115        );
2116    }
2117}
2118
2119#[inline(always)]
2120fn fill_bottom_flat_triangle_zbuffered<
2121    D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>,
2122>(
2123    p1: Point,
2124    p2: Point,
2125    p3: Point,
2126    z1: u32,
2127    z2: u32,
2128    z3: u32,
2129    color: embedded_graphics_core::pixelcolor::Rgb565,
2130    fb: &mut D,
2131    zbuffer: &mut [u32],
2132    width: usize,
2133    fog_config: Option<&FogConfig>,
2134    dither_config: Option<&DitherConfig>,
2135) where
2136    <D as DrawTarget>::Error: Debug,
2137{
2138    let height = p2.y - p1.y;
2139    if height == 0 {
2140        return;
2141    }
2142
2143    // Use fixed-point arithmetic (16.16 format) for edge slopes
2144    // This avoids floating-point operations entirely
2145    let invslope1 = ((p2.x - p1.x) << 16) / height;
2146    let invslope2 = ((p3.x - p1.x) << 16) / height;
2147
2148    let mut curx1 = p1.x << 16; // Fixed-point
2149    let mut curx2 = p1.x << 16; // Fixed-point
2150
2151    for scanline_y in p1.y..=p2.y {
2152        let dy = scanline_y - p1.y;
2153        // Integer interpolation for Z using only integer math
2154        let z_left = if height > 0 {
2155            (z1 as i64 + ((z2 as i64 - z1 as i64) * dy as i64 / height as i64)) as u32
2156        } else {
2157            z1
2158        };
2159        let z_right = if height > 0 {
2160            (z1 as i64 + ((z3 as i64 - z1 as i64) * dy as i64 / height as i64)) as u32
2161        } else {
2162            z1
2163        };
2164
2165        draw_scanline_zbuffered(
2166            curx1 >> 16, // Convert back from fixed-point
2167            curx2 >> 16, // Convert back from fixed-point
2168            scanline_y,
2169            z_left,
2170            z_right,
2171            color,
2172            fb,
2173            zbuffer,
2174            width,
2175            fog_config,
2176            dither_config,
2177        );
2178
2179        curx1 += invslope1;
2180        curx2 += invslope2;
2181    }
2182}
2183
2184#[inline(always)]
2185fn fill_top_flat_triangle_zbuffered<
2186    D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>,
2187>(
2188    p1: Point,
2189    p2: Point,
2190    p3: Point,
2191    z1: u32,
2192    z2: u32,
2193    z3: u32,
2194    color: embedded_graphics_core::pixelcolor::Rgb565,
2195    fb: &mut D,
2196    zbuffer: &mut [u32],
2197    width: usize,
2198    fog_config: Option<&FogConfig>,
2199    dither_config: Option<&DitherConfig>,
2200) where
2201    <D as DrawTarget>::Error: Debug,
2202{
2203    let height = p3.y - p1.y;
2204    if height == 0 {
2205        return;
2206    }
2207
2208    // Use fixed-point arithmetic (16.16 format) for edge slopes
2209    let invslope1 = ((p3.x - p1.x) << 16) / height;
2210    let invslope2 = ((p3.x - p2.x) << 16) / height;
2211
2212    let mut curx1 = p3.x << 16; // Fixed-point
2213    let mut curx2 = p3.x << 16; // Fixed-point
2214
2215    for scanline_y in (p1.y..=p3.y).rev() {
2216        let dy = scanline_y - p1.y;
2217        // Integer interpolation for Z using only integer math
2218        let z_left = if height > 0 {
2219            (z1 as i64 + ((z3 as i64 - z1 as i64) * dy as i64 / height as i64)) as u32
2220        } else {
2221            z1
2222        };
2223        let z_right = if height > 0 {
2224            (z2 as i64 + ((z3 as i64 - z2 as i64) * dy as i64 / height as i64)) as u32
2225        } else {
2226            z2
2227        };
2228
2229        draw_scanline_zbuffered(
2230            curx1 >> 16, // Convert back from fixed-point
2231            curx2 >> 16, // Convert back from fixed-point
2232            scanline_y,
2233            z_left,
2234            z_right,
2235            color,
2236            fb,
2237            zbuffer,
2238            width,
2239            fog_config,
2240            dither_config,
2241        );
2242
2243        curx1 -= invslope1;
2244        curx2 -= invslope2;
2245    }
2246}
2247
2248// Gouraud shaded bottom-flat triangle with z-buffering
2249#[inline(always)]
2250fn fill_bottom_flat_triangle_zbuffered_gouraud<
2251    D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>,
2252>(
2253    p1: Point,
2254    p2: Point,
2255    p3: Point,
2256    z1: u32,
2257    z2: u32,
2258    z3: u32,
2259    c1: embedded_graphics_core::pixelcolor::Rgb565,
2260    c2: embedded_graphics_core::pixelcolor::Rgb565,
2261    c3: embedded_graphics_core::pixelcolor::Rgb565,
2262    fb: &mut D,
2263    zbuffer: &mut [u32],
2264    width: usize,
2265    fog_config: Option<&FogConfig>,
2266    dither_config: Option<&DitherConfig>,
2267) where
2268    <D as DrawTarget>::Error: Debug,
2269{
2270    let height = p2.y - p1.y;
2271    if height == 0 {
2272        return;
2273    }
2274
2275    let invslope1 = ((p2.x - p1.x) << 16) / height;
2276    let invslope2 = ((p3.x - p1.x) << 16) / height;
2277
2278    let mut curx1 = p1.x << 16;
2279    let mut curx2 = p1.x << 16;
2280
2281    for scanline_y in p1.y..=p2.y {
2282        let dy = scanline_y - p1.y;
2283        let t = dy as f32 / height as f32;
2284
2285        // Interpolate Z values
2286        let z_left = if height > 0 {
2287            (z1 as i64 + ((z2 as i64 - z1 as i64) * dy as i64 / height as i64)) as u32
2288        } else {
2289            z1
2290        };
2291        let z_right = if height > 0 {
2292            (z1 as i64 + ((z3 as i64 - z1 as i64) * dy as i64 / height as i64)) as u32
2293        } else {
2294            z1
2295        };
2296
2297        // Interpolate colors
2298        let color_left = interpolate_color(c1, c2, t);
2299        let color_right = interpolate_color(c1, c3, t);
2300
2301        draw_scanline_zbuffered_gouraud(
2302            curx1 >> 16,
2303            curx2 >> 16,
2304            scanline_y,
2305            z_left,
2306            z_right,
2307            color_left,
2308            color_right,
2309            fb,
2310            zbuffer,
2311            width,
2312            fog_config,
2313            dither_config,
2314        );
2315
2316        curx1 += invslope1;
2317        curx2 += invslope2;
2318    }
2319}
2320
2321// Gouraud shaded top-flat triangle with z-buffering
2322#[inline(always)]
2323fn fill_top_flat_triangle_zbuffered_gouraud<
2324    D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>,
2325>(
2326    p1: Point,
2327    p2: Point,
2328    p3: Point,
2329    z1: u32,
2330    z2: u32,
2331    z3: u32,
2332    c1: embedded_graphics_core::pixelcolor::Rgb565,
2333    c2: embedded_graphics_core::pixelcolor::Rgb565,
2334    c3: embedded_graphics_core::pixelcolor::Rgb565,
2335    fb: &mut D,
2336    zbuffer: &mut [u32],
2337    width: usize,
2338    fog_config: Option<&FogConfig>,
2339    dither_config: Option<&DitherConfig>,
2340) where
2341    <D as DrawTarget>::Error: Debug,
2342{
2343    let height = p3.y - p1.y;
2344    if height == 0 {
2345        return;
2346    }
2347
2348    let invslope1 = ((p3.x - p1.x) << 16) / height;
2349    let invslope2 = ((p3.x - p2.x) << 16) / height;
2350
2351    let mut curx1 = p3.x << 16;
2352    let mut curx2 = p3.x << 16;
2353
2354    for scanline_y in (p1.y..=p3.y).rev() {
2355        let dy = scanline_y - p1.y;
2356        let t = dy as f32 / height as f32;
2357
2358        // Interpolate Z values
2359        let z_left = if height > 0 {
2360            (z1 as i64 + ((z3 as i64 - z1 as i64) * dy as i64 / height as i64)) as u32
2361        } else {
2362            z1
2363        };
2364        let z_right = if height > 0 {
2365            (z2 as i64 + ((z3 as i64 - z2 as i64) * dy as i64 / height as i64)) as u32
2366        } else {
2367            z2
2368        };
2369
2370        // Interpolate colors
2371        let color_left = interpolate_color(c1, c3, t);
2372        let color_right = interpolate_color(c2, c3, t);
2373
2374        draw_scanline_zbuffered_gouraud(
2375            curx1 >> 16,
2376            curx2 >> 16,
2377            scanline_y,
2378            z_left,
2379            z_right,
2380            color_left,
2381            color_right,
2382            fb,
2383            zbuffer,
2384            width,
2385            fog_config,
2386            dither_config,
2387        );
2388
2389        curx1 -= invslope1;
2390        curx2 -= invslope2;
2391    }
2392}
2393
2394// Draw scanline with Gouraud shading and z-buffering
2395#[inline(always)]
2396fn draw_scanline_zbuffered_gouraud<
2397    D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>,
2398>(
2399    x1: i32,
2400    x2: i32,
2401    y: i32,
2402    z1: u32,
2403    z2: u32,
2404    color1: embedded_graphics_core::pixelcolor::Rgb565,
2405    color2: embedded_graphics_core::pixelcolor::Rgb565,
2406    fb: &mut D,
2407    zbuffer: &mut [u32],
2408    width: usize,
2409    fog_config: Option<&FogConfig>,
2410    dither_config: Option<&DitherConfig>,
2411) where
2412    <D as DrawTarget>::Error: Debug,
2413{
2414    let start = x1.min(x2);
2415    let end = x1.max(x2);
2416    let span_width = end - start;
2417
2418    for x in start..=end {
2419        if x < 0 || y < 0 {
2420            continue;
2421        }
2422
2423        let zbuffer_idx = y as usize * width + x as usize;
2424        if zbuffer_idx >= zbuffer.len() {
2425            continue;
2426        }
2427
2428        // Interpolate Z value
2429        let t = if span_width > 0 {
2430            (x - start) as f32 / span_width as f32
2431        } else {
2432            0.0
2433        };
2434        let z = if span_width > 0 {
2435            (z1 as i64 + ((z2 as i64 - z1 as i64) * (x - start) as i64 / span_width as i64)) as u32
2436        } else {
2437            z1
2438        };
2439
2440        // Z-test with epsilon to prevent Z-fighting on nearly coplanar faces
2441        // We make the test slightly more permissive by allowing pixels within epsilon range
2442        if z < zbuffer[zbuffer_idx].saturating_add(DEPTH_EPSILON) {
2443            zbuffer[zbuffer_idx] = z;
2444
2445            // Interpolate color
2446            let mut final_color = interpolate_color(color1, color2, t);
2447
2448            // Apply effects in order: fog first, then dithering
2449            if let Some(fog) = fog_config {
2450                final_color = fog.apply(final_color, z);
2451            }
2452
2453            if let Some(dither) = dither_config {
2454                final_color = dither.apply(final_color, x, y);
2455            }
2456
2457            fb.draw_iter([embedded_graphics_core::Pixel(Point::new(x, y), final_color)])
2458                .unwrap();
2459        }
2460    }
2461}
2462
2463#[inline(always)]
2464fn draw_scanline_zbuffered<D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>>(
2465    x1: i32,
2466    x2: i32,
2467    y: i32,
2468    z1: u32,
2469    z2: u32,
2470    color: embedded_graphics_core::pixelcolor::Rgb565,
2471    fb: &mut D,
2472    zbuffer: &mut [u32],
2473    width: usize,
2474    fog_config: Option<&FogConfig>,
2475    dither_config: Option<&DitherConfig>,
2476) where
2477    <D as DrawTarget>::Error: Debug,
2478{
2479    let start = x1.min(x2);
2480    let end = x1.max(x2);
2481
2482    for x in start..=end {
2483        if x < 0 || y < 0 {
2484            continue;
2485        }
2486
2487        let zbuffer_idx = y as usize * width + x as usize;
2488        if zbuffer_idx >= zbuffer.len() {
2489            continue;
2490        }
2491
2492        // Integer interpolation for Z (fixed-point 16.16 format)
2493        // This avoids floating-point in the inner loop
2494        let span = end - start;
2495        let z = if span == 0 {
2496            z1
2497        } else {
2498            // Linear interpolation using integer math
2499            let t_num = (x - start) as i64;
2500            let t_den = span as i64;
2501            let z_diff = z2 as i64 - z1 as i64;
2502            (z1 as i64 + (t_num * z_diff / t_den)) as u32
2503        };
2504
2505        // Z-buffer test with epsilon to prevent Z-fighting (closer = smaller depth value)
2506        // We make the test slightly more permissive by allowing pixels within epsilon range
2507        if z < zbuffer[zbuffer_idx].saturating_add(DEPTH_EPSILON) {
2508            zbuffer[zbuffer_idx] = z;
2509
2510            // Apply effects in order: fog first, then dithering
2511            let mut final_color = color;
2512
2513            if let Some(fog) = fog_config {
2514                final_color = fog.apply(final_color, z);
2515            }
2516
2517            if let Some(dither) = dither_config {
2518                final_color = dither.apply(final_color, x, y);
2519            }
2520
2521            fb.draw_iter([embedded_graphics_core::Pixel(Point::new(x, y), final_color)])
2522                .unwrap();
2523        }
2524    }
2525}
2526
2527// Textured triangle rendering with z-buffering
2528#[inline(always)]
2529fn fill_triangle_zbuffered_textured<
2530    D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>,
2531>(
2532    p1: nalgebra::Point2<i32>,
2533    p2: nalgebra::Point2<i32>,
2534    p3: nalgebra::Point2<i32>,
2535    z1: f32,
2536    z2: f32,
2537    z3: f32,
2538    w1: f32,
2539    w2: f32,
2540    w3: f32,
2541    uv1: [f32; 2],
2542    uv2: [f32; 2],
2543    uv3: [f32; 2],
2544    texture: &crate::texture::Texture,
2545    fb: &mut D,
2546    zbuffer: &mut [u32],
2547    width: usize,
2548    fog_config: Option<&FogConfig>,
2549    dither_config: Option<&DitherConfig>,
2550) where
2551    <D as DrawTarget>::Error: Debug,
2552{
2553    // Convert to embedded_graphics Points
2554    let p1_eg = Point::new(p1.x, p1.y);
2555    let p2_eg = Point::new(p2.x, p2.y);
2556    let p3_eg = Point::new(p3.x, p3.y);
2557
2558    // Convert float depths to fixed-point integers (16.16 format)
2559    let z1_int = (z1 * 65536.0) as u32;
2560    let z2_int = (z2 * 65536.0) as u32;
2561    let z3_int = (z3 * 65536.0) as u32;
2562
2563    // Handle flat triangles
2564    if p2_eg.y == p3_eg.y {
2565        fill_bottom_flat_triangle_zbuffered_textured(
2566            p1_eg,
2567            p2_eg,
2568            p3_eg,
2569            z1_int,
2570            z2_int,
2571            z3_int,
2572            w1,
2573            w2,
2574            w3,
2575            uv1,
2576            uv2,
2577            uv3,
2578            texture,
2579            fb,
2580            zbuffer,
2581            width,
2582            fog_config,
2583            dither_config,
2584        );
2585    } else if p1_eg.y == p2_eg.y {
2586        fill_top_flat_triangle_zbuffered_textured(
2587            p1_eg,
2588            p2_eg,
2589            p3_eg,
2590            z1_int,
2591            z2_int,
2592            z3_int,
2593            w1,
2594            w2,
2595            w3,
2596            uv1,
2597            uv2,
2598            uv3,
2599            texture,
2600            fb,
2601            zbuffer,
2602            width,
2603            fog_config,
2604            dither_config,
2605        );
2606    } else {
2607        // Split into two flat triangles
2608        let t = (p2_eg.y - p1_eg.y) as f32 / (p3_eg.y - p1_eg.y) as f32;
2609        let p4 = Point::new(
2610            (p1_eg.x as f32 + t * (p3_eg.x - p1_eg.x) as f32) as i32,
2611            p2_eg.y,
2612        );
2613        let z4_int = (z1_int as i64 + (t * (z3_int as i64 - z1_int as i64) as f32) as i64) as u32;
2614        // Interpolate W at split point
2615        let w4 = w1 + t * (w3 - w1);
2616        // Interpolate UV at split point
2617        let uv4 = [
2618            uv1[0] + t * (uv3[0] - uv1[0]),
2619            uv1[1] + t * (uv3[1] - uv1[1]),
2620        ];
2621
2622        fill_bottom_flat_triangle_zbuffered_textured(
2623            p1_eg,
2624            p2_eg,
2625            p4,
2626            z1_int,
2627            z2_int,
2628            z4_int,
2629            w1,
2630            w2,
2631            w4,
2632            uv1,
2633            uv2,
2634            uv4,
2635            texture,
2636            fb,
2637            zbuffer,
2638            width,
2639            fog_config,
2640            dither_config,
2641        );
2642        fill_top_flat_triangle_zbuffered_textured(
2643            p2_eg,
2644            p4,
2645            p3_eg,
2646            z2_int,
2647            z4_int,
2648            z3_int,
2649            w2,
2650            w4,
2651            w3,
2652            uv2,
2653            uv4,
2654            uv3,
2655            texture,
2656            fb,
2657            zbuffer,
2658            width,
2659            fog_config,
2660            dither_config,
2661        );
2662    }
2663}
2664
2665// Textured bottom-flat triangle with z-buffering
2666#[inline(always)]
2667fn fill_bottom_flat_triangle_zbuffered_textured<
2668    D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>,
2669>(
2670    p1: Point,
2671    p2: Point,
2672    p3: Point,
2673    z1: u32,
2674    z2: u32,
2675    z3: u32,
2676    w1: f32,
2677    w2: f32,
2678    w3: f32,
2679    uv1: [f32; 2],
2680    uv2: [f32; 2],
2681    uv3: [f32; 2],
2682    texture: &crate::texture::Texture,
2683    fb: &mut D,
2684    zbuffer: &mut [u32],
2685    width: usize,
2686    fog_config: Option<&FogConfig>,
2687    dither_config: Option<&DitherConfig>,
2688) where
2689    <D as DrawTarget>::Error: Debug,
2690{
2691    let height = p2.y - p1.y;
2692    if height == 0 {
2693        return;
2694    }
2695
2696    let invslope1 = ((p2.x - p1.x) << 16) / height;
2697    let invslope2 = ((p3.x - p1.x) << 16) / height;
2698
2699    let mut curx1 = p1.x << 16;
2700    let mut curx2 = p1.x << 16;
2701
2702    for scanline_y in p1.y..=p2.y {
2703        let dy = scanline_y - p1.y;
2704        let t = dy as f32 / height as f32;
2705
2706        // Interpolate Z values
2707        let z_left = if height > 0 {
2708            (z1 as i64 + ((z2 as i64 - z1 as i64) * dy as i64 / height as i64)) as u32
2709        } else {
2710            z1
2711        };
2712        let z_right = if height > 0 {
2713            (z1 as i64 + ((z3 as i64 - z1 as i64) * dy as i64 / height as i64)) as u32
2714        } else {
2715            z1
2716        };
2717
2718        // Interpolate W values
2719        let w_left = w1 + t * (w2 - w1);
2720        let w_right = w1 + t * (w3 - w1);
2721
2722        // Interpolate UVs
2723        let uv_left = [
2724            uv1[0] + t * (uv2[0] - uv1[0]),
2725            uv1[1] + t * (uv2[1] - uv1[1]),
2726        ];
2727        let uv_right = [
2728            uv1[0] + t * (uv3[0] - uv1[0]),
2729            uv1[1] + t * (uv3[1] - uv1[1]),
2730        ];
2731
2732        draw_scanline_zbuffered_textured(
2733            curx1 >> 16,
2734            curx2 >> 16,
2735            scanline_y,
2736            z_left,
2737            z_right,
2738            w_left,
2739            w_right,
2740            uv_left,
2741            uv_right,
2742            texture,
2743            fb,
2744            zbuffer,
2745            width,
2746            fog_config,
2747            dither_config,
2748        );
2749
2750        curx1 += invslope1;
2751        curx2 += invslope2;
2752    }
2753}
2754
2755// Textured top-flat triangle with z-buffering
2756#[inline(always)]
2757fn fill_top_flat_triangle_zbuffered_textured<
2758    D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>,
2759>(
2760    p1: Point,
2761    p2: Point,
2762    p3: Point,
2763    z1: u32,
2764    z2: u32,
2765    z3: u32,
2766    w1: f32,
2767    w2: f32,
2768    w3: f32,
2769    uv1: [f32; 2],
2770    uv2: [f32; 2],
2771    uv3: [f32; 2],
2772    texture: &crate::texture::Texture,
2773    fb: &mut D,
2774    zbuffer: &mut [u32],
2775    width: usize,
2776    fog_config: Option<&FogConfig>,
2777    dither_config: Option<&DitherConfig>,
2778) where
2779    <D as DrawTarget>::Error: Debug,
2780{
2781    let height = p3.y - p1.y;
2782    if height == 0 {
2783        return;
2784    }
2785
2786    let invslope1 = ((p3.x - p1.x) << 16) / height;
2787    let invslope2 = ((p3.x - p2.x) << 16) / height;
2788
2789    let mut curx1 = p3.x << 16;
2790    let mut curx2 = p3.x << 16;
2791
2792    for scanline_y in (p1.y..=p3.y).rev() {
2793        let dy = scanline_y - p1.y;
2794        let t = dy as f32 / height as f32;
2795
2796        // Interpolate Z values
2797        let z_left = if height > 0 {
2798            (z1 as i64 + ((z3 as i64 - z1 as i64) * dy as i64 / height as i64)) as u32
2799        } else {
2800            z1
2801        };
2802        let z_right = if height > 0 {
2803            (z2 as i64 + ((z3 as i64 - z2 as i64) * dy as i64 / height as i64)) as u32
2804        } else {
2805            z2
2806        };
2807
2808        // Interpolate W values
2809        let w_left = w1 + t * (w3 - w1);
2810        let w_right = w2 + t * (w3 - w2);
2811
2812        // Interpolate UVs
2813        let uv_left = [
2814            uv1[0] + t * (uv3[0] - uv1[0]),
2815            uv1[1] + t * (uv3[1] - uv1[1]),
2816        ];
2817        let uv_right = [
2818            uv2[0] + t * (uv3[0] - uv2[0]),
2819            uv2[1] + t * (uv3[1] - uv2[1]),
2820        ];
2821
2822        draw_scanline_zbuffered_textured(
2823            curx1 >> 16,
2824            curx2 >> 16,
2825            scanline_y,
2826            z_left,
2827            z_right,
2828            w_left,
2829            w_right,
2830            uv_left,
2831            uv_right,
2832            texture,
2833            fb,
2834            zbuffer,
2835            width,
2836            fog_config,
2837            dither_config,
2838        );
2839
2840        curx1 -= invslope1;
2841        curx2 -= invslope2;
2842    }
2843}
2844
2845// Draw scanline with texture mapping and z-buffering
2846#[inline(always)]
2847fn draw_scanline_zbuffered_textured<
2848    D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>,
2849>(
2850    x1: i32,
2851    x2: i32,
2852    y: i32,
2853    z1: u32,
2854    z2: u32,
2855    w1: f32,
2856    w2: f32,
2857    uv1: [f32; 2],
2858    uv2: [f32; 2],
2859    texture: &crate::texture::Texture,
2860    fb: &mut D,
2861    zbuffer: &mut [u32],
2862    width: usize,
2863    fog_config: Option<&FogConfig>,
2864    dither_config: Option<&DitherConfig>,
2865) where
2866    <D as DrawTarget>::Error: Debug,
2867{
2868    let start = x1.min(x2);
2869    let end = x1.max(x2);
2870    let span_width = end - start;
2871
2872    for x in start..=end {
2873        if x < 0 || y < 0 {
2874            continue;
2875        }
2876
2877        let zbuffer_idx = y as usize * width + x as usize;
2878        if zbuffer_idx >= zbuffer.len() {
2879            continue;
2880        }
2881
2882        // Interpolate Z value
2883        let t = if span_width > 0 {
2884            (x - start) as f32 / span_width as f32
2885        } else {
2886            0.0
2887        };
2888        let z = if span_width > 0 {
2889            (z1 as i64 + ((z2 as i64 - z1 as i64) * (x - start) as i64 / span_width as i64)) as u32
2890        } else {
2891            z1
2892        };
2893
2894        // Z-test with epsilon to prevent Z-fighting on nearly coplanar faces
2895        // We make the test slightly more permissive by allowing pixels within epsilon range
2896        if z < zbuffer[zbuffer_idx].saturating_add(DEPTH_EPSILON) {
2897            zbuffer[zbuffer_idx] = z;
2898
2899            // Perspective-correct UV interpolation
2900            let ow1 = 1.0 / w1;
2901            let ow2 = 1.0 / w2;
2902            let one_over_w = ow1 + t * (ow2 - ow1);
2903            let u = (uv1[0] * ow1 + t * (uv2[0] * ow2 - uv1[0] * ow1)) / one_over_w;
2904            let v = (uv1[1] * ow1 + t * (uv2[1] * ow2 - uv1[1] * ow1)) / one_over_w;
2905
2906            // Sample texture
2907            let mut final_color = texture.sample(u, v);
2908
2909            // Apply effects in order: fog first, then dithering
2910            if let Some(fog) = fog_config {
2911                final_color = fog.apply(final_color, z);
2912            }
2913
2914            if let Some(dither) = dither_config {
2915                final_color = dither.apply(final_color, x, y);
2916            }
2917
2918            fb.draw_iter([embedded_graphics_core::Pixel(Point::new(x, y), final_color)])
2919                .unwrap();
2920        }
2921    }
2922}
2923
2924#[cfg(test)]
2925mod tests {
2926    extern crate std;
2927    use super::*;
2928    use embedded_graphics_core::pixelcolor::Rgb565;
2929    use embedded_graphics_core::prelude::*;
2930    use nalgebra::Point2;
2931
2932    // Mock framebuffer for testing
2933    struct MockFramebuffer {
2934        pixels: std::vec::Vec<(i32, i32, Rgb565)>,
2935    }
2936
2937    impl MockFramebuffer {
2938        fn new() -> Self {
2939            Self {
2940                pixels: std::vec::Vec::new(),
2941            }
2942        }
2943
2944        fn contains_pixel(&self, x: i32, y: i32) -> bool {
2945            self.pixels.iter().any(|(px, py, _)| *px == x && *py == y)
2946        }
2947
2948        fn pixel_count(&self) -> usize {
2949            self.pixels.len()
2950        }
2951    }
2952
2953    impl DrawTarget for MockFramebuffer {
2954        type Color = Rgb565;
2955        type Error = core::convert::Infallible;
2956
2957        fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
2958        where
2959            I: IntoIterator<Item = embedded_graphics_core::Pixel<Self::Color>>,
2960        {
2961            for pixel in pixels {
2962                self.pixels.push((pixel.0.x, pixel.0.y, pixel.1));
2963            }
2964            Ok(())
2965        }
2966    }
2967
2968    impl OriginDimensions for MockFramebuffer {
2969        fn size(&self) -> Size {
2970            Size::new(640, 480)
2971        }
2972    }
2973
2974    #[test]
2975    fn test_draw_point() {
2976        let mut fb = MockFramebuffer::new();
2977        let point = Point2::new(10, 20);
2978        let color = Rgb565::CSS_RED;
2979
2980        draw(DrawPrimitive::ColoredPoint(point, color), &mut fb);
2981
2982        assert_eq!(fb.pixel_count(), 1);
2983        assert!(fb.contains_pixel(10, 20));
2984    }
2985
2986    #[test]
2987    fn test_draw_line_horizontal() {
2988        let mut fb = MockFramebuffer::new();
2989        let p1 = Point2::new(10, 20);
2990        let p2 = Point2::new(20, 20);
2991        let color = Rgb565::CSS_GREEN;
2992
2993        draw(DrawPrimitive::Line([p1, p2], color), &mut fb);
2994
2995        // Should draw pixels along the horizontal line
2996        assert!(fb.pixel_count() >= 10); // At least 10 pixels
2997        assert!(fb.contains_pixel(10, 20));
2998        assert!(fb.contains_pixel(20, 20));
2999    }
3000
3001    #[test]
3002    fn test_draw_line_vertical() {
3003        let mut fb = MockFramebuffer::new();
3004        let p1 = Point2::new(10, 10);
3005        let p2 = Point2::new(10, 20);
3006        let color = Rgb565::CSS_BLUE;
3007
3008        draw(DrawPrimitive::Line([p1, p2], color), &mut fb);
3009
3010        // Should draw pixels along the vertical line
3011        assert!(fb.pixel_count() >= 10);
3012        assert!(fb.contains_pixel(10, 10));
3013        assert!(fb.contains_pixel(10, 20));
3014    }
3015
3016    #[test]
3017    fn test_draw_line_diagonal() {
3018        let mut fb = MockFramebuffer::new();
3019        let p1 = Point2::new(0, 0);
3020        let p2 = Point2::new(10, 10);
3021        let color = Rgb565::CSS_WHITE;
3022
3023        draw(DrawPrimitive::Line([p1, p2], color), &mut fb);
3024
3025        // Should draw pixels along the diagonal
3026        assert!(fb.pixel_count() >= 10);
3027        assert!(fb.contains_pixel(0, 0));
3028        assert!(fb.contains_pixel(10, 10));
3029    }
3030
3031    #[test]
3032    fn test_draw_triangle_flat_bottom() {
3033        let mut fb = MockFramebuffer::new();
3034        let vertices = [
3035            Point2::new(50, 10), // Top vertex
3036            Point2::new(30, 30), // Bottom left
3037            Point2::new(70, 30), // Bottom right
3038        ];
3039        let color = Rgb565::CSS_YELLOW;
3040
3041        draw(DrawPrimitive::ColoredTriangle(vertices, color), &mut fb);
3042
3043        // Should draw multiple pixels for the filled triangle
3044        let count = fb.pixel_count();
3045        assert!(count > 0, "Expected pixels to be drawn, got {}", count);
3046        // Top vertex should be drawn
3047        assert!(fb.contains_pixel(50, 10));
3048    }
3049
3050    #[test]
3051    fn test_draw_triangle_flat_top() {
3052        let mut fb = MockFramebuffer::new();
3053        let vertices = [
3054            Point2::new(30, 10), // Top left
3055            Point2::new(70, 10), // Top right
3056            Point2::new(50, 30), // Bottom vertex
3057        ];
3058        let color = Rgb565::CSS_CYAN;
3059
3060        draw(DrawPrimitive::ColoredTriangle(vertices, color), &mut fb);
3061
3062        // Should draw multiple pixels for the filled triangle
3063        assert!(fb.pixel_count() > 20);
3064        assert!(fb.contains_pixel(50, 30));
3065    }
3066
3067    #[test]
3068    fn test_draw_triangle_general() {
3069        let mut fb = MockFramebuffer::new();
3070        let vertices = [
3071            Point2::new(50, 10),
3072            Point2::new(30, 30),
3073            Point2::new(80, 40),
3074        ];
3075        let color = Rgb565::CSS_MAGENTA;
3076
3077        draw(DrawPrimitive::ColoredTriangle(vertices, color), &mut fb);
3078
3079        // Should draw many pixels for the filled triangle
3080        assert!(fb.pixel_count() > 30);
3081    }
3082
3083    #[test]
3084    fn test_triangle_vertex_sorting() {
3085        let mut fb = MockFramebuffer::new();
3086        // Vertices in reverse y order
3087        let vertices = [
3088            Point2::new(50, 30), // Bottom (will be sorted to top)
3089            Point2::new(30, 10), // Top
3090            Point2::new(70, 20), // Middle
3091        ];
3092        let color = Rgb565::CSS_WHITE;
3093
3094        // Should not panic and should draw the triangle correctly
3095        draw(DrawPrimitive::ColoredTriangle(vertices, color), &mut fb);
3096
3097        assert!(fb.pixel_count() > 10);
3098    }
3099
3100    #[test]
3101    fn test_draw_multiple_primitives() {
3102        let mut fb = MockFramebuffer::new();
3103
3104        draw(
3105            DrawPrimitive::ColoredPoint(Point2::new(5, 5), Rgb565::CSS_RED),
3106            &mut fb,
3107        );
3108        draw(
3109            DrawPrimitive::Line(
3110                [Point2::new(10, 10), Point2::new(20, 20)],
3111                Rgb565::CSS_GREEN,
3112            ),
3113            &mut fb,
3114        );
3115
3116        // Should have pixels from both primitives
3117        assert!(fb.pixel_count() > 11); // 1 point + at least 10 from line
3118        assert!(fb.contains_pixel(5, 5));
3119    }
3120}