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;
30use embedded_graphics_core::pixelcolor::Rgb565;
31use embedded_graphics_core::pixelcolor::RgbColor;
32use embedded_graphics_core::prelude::Point;
33use heapless::Vec;
34
35use crate::DrawPrimitive;
36#[cfg(feature = "textured")]
37use crate::retro::{PaletteMode, ScreenTint, StippleMode, TextureMapping};
38
39/// Framebuffer that supports reading back pixel values.
40///
41/// Required by the analytical-AA rasterizers (`draw_zbuffered_aa`,
42/// `draw_line_aa`) which blend partial-coverage triangle edges and
43/// anti-aliased line endpoints with the existing framebuffer contents.
44/// Implementers should return the most recently written color at `point`,
45/// or `Rgb565::BLACK` for out-of-bounds reads.
46///
47/// Prefer implementing [`PixelRead`], re-exported from
48/// [`embedded_draw_target`]: it is the same trait `embedded-gui` and the wider
49/// ecosystem use, so one buffer can serve this crate's rasterizers, a GUI
50/// library's alpha compositor, and plain `embedded-graphics` drawing at once.
51/// Every `PixelRead<Color = Rgb565>` target satisfies `ReadPixel` through the
52/// blanket impl below, so both work everywhere this crate asks for readback.
53#[cfg(feature = "aa")]
54pub trait ReadPixel {
55    /// Returns the color currently stored at `point`.
56    fn read_pixel(&self, point: Point) -> Rgb565;
57}
58
59/// Readback capability shared with the rest of the ecosystem.
60///
61/// Implementing this is the recommended way to satisfy the AA rasterizers'
62/// [`ReadPixel`] bound; `ReadPixel` itself remains for existing implementors
63/// and is slated for removal in 0.5.
64pub use embedded_draw_target::PixelRead;
65
66#[cfg(feature = "aa")]
67impl<T: PixelRead<Color = Rgb565>> ReadPixel for T {
68    #[inline]
69    fn read_pixel(&self, point: Point) -> Rgb565 {
70        self.get_pixel(point)
71    }
72}
73
74/// Fast RGB565 alpha blending.
75///
76/// Blends `fg` color over `bg` color using an 8-bit alpha value `alpha` ∈ \[0, 255\].
77#[inline(always)]
78pub fn fast_blend_rgb565(bg: Rgb565, fg: Rgb565, alpha: u8) -> Rgb565 {
79    if alpha == 255 {
80        return fg;
81    }
82    if alpha == 0 {
83        return bg;
84    }
85    let a = alpha as u32;
86    let inv = 255 - a;
87    let r = (bg.r() as u32 * inv + fg.r() as u32 * a) / 255;
88    let g = (bg.g() as u32 * inv + fg.g() as u32 * a) / 255;
89    let b = (bg.b() as u32 * inv + fg.b() as u32 * a) / 255;
90    Rgb565::new(r as u8, g as u8, b as u8)
91}
92
93/// Fast RGBA8888 alpha blending.
94///
95/// Blends `fg` RGBA8888 channel tuple over `bg` RGBA8888 channel tuple.
96#[inline(always)]
97pub fn fast_blend_rgba8888(bg: [u8; 4], fg: [u8; 4]) -> [u8; 4] {
98    let a = fg[3] as u32;
99    if a == 255 {
100        return fg;
101    }
102    if a == 0 {
103        return bg;
104    }
105    let inv = 255 - a;
106    let r = (bg[0] as u32 * inv + fg[0] as u32 * a) / 255;
107    let g = (bg[1] as u32 * inv + fg[1] as u32 * a) / 255;
108    let b = (bg[2] as u32 * inv + fg[2] as u32 * a) / 255;
109    let out_a = fg[3] as u32 + (bg[3] as u32 * inv) / 255;
110    [r as u8, g as u8, b as u8, out_a as u8]
111}
112
113/// Fast RGBA8888 to RGB565 alpha blending.
114///
115/// Blends an RGBA8888 foreground pixel directly onto an RGB565 background pixel.
116#[inline(always)]
117pub fn fast_blend_rgba8888_to_rgb565(bg: Rgb565, fg_rgba: [u8; 4]) -> Rgb565 {
118    let alpha = fg_rgba[3];
119    if alpha == 0 {
120        return bg;
121    }
122    let fg_r = fg_rgba[0] >> 3;
123    let fg_g = fg_rgba[1] >> 2;
124    let fg_b = fg_rgba[2] >> 3;
125    let fg_565 = Rgb565::new(fg_r, fg_g, fg_b);
126    fast_blend_rgb565(bg, fg_565, alpha)
127}
128
129/// Fast color inversion (reverse color) filter for RGB565.
130///
131/// Inverts RGB color channels.
132#[inline(always)]
133pub fn reverse_color_rgb565(c: Rgb565) -> Rgb565 {
134    Rgb565::new(31 - c.r(), 63 - c.g(), 31 - c.b())
135}
136
137/// Fast color inversion (reverse color) filter for RGBA8888.
138///
139/// Inverts R, G, B channels while preserving alpha.
140#[inline(always)]
141pub fn reverse_color_rgba8888(rgba: [u8; 4]) -> [u8; 4] {
142    [255 - rgba[0], 255 - rgba[1], 255 - rgba[2], rgba[3]]
143}
144
145/// Component-wise blend in 8-bit fixed-point coverage.
146/// `coverage_q8` ∈ [0, 256]; 256 = full triangle color, 0 = full background.
147#[cfg(feature = "aa")]
148#[inline(always)]
149fn blend_q8(bg: Rgb565, fg: Rgb565, coverage_q8: u32) -> Rgb565 {
150    let inv = 256 - coverage_q8;
151    let r = (bg.r() as u32 * inv + fg.r() as u32 * coverage_q8) >> 8;
152    let g = (bg.g() as u32 * inv + fg.g() as u32 * coverage_q8) >> 8;
153    let b = (bg.b() as u32 * inv + fg.b() as u32 * coverage_q8) >> 8;
154    Rgb565::new(r as u8, g as u8, b as u8)
155}
156
157/// Z-test + coverage blend + write a single AA pixel.
158///
159/// Coverage handling has three cases:
160/// - Full coverage (`coverage_q8 >= 256`): fast path, write color directly.
161/// - Partial coverage on a virgin pixel (z-buffer at `u32::MAX`): true
162///   silhouette against the background — blend `bg * (1-c) + color * c`.
163/// - Partial coverage on a pixel another triangle has already painted:
164///   treat as a shared interior edge and write full color. This avoids the
165///   classic double-blend seam artifact at shared edges in closed meshes.
166///   Tradeoff: thin protrusions whose silhouette overlaps another triangle
167///   lose AA on that overlap. Acceptable for typical closed geometry.
168#[cfg(feature = "aa-heuristic")]
169#[inline(always)]
170fn aa_pixel<D>(
171    fb: &mut D,
172    x: i32,
173    y: i32,
174    color: Rgb565,
175    z: u32,
176    zbuffer: &mut [crate::ZDepth],
177    width: usize,
178    coverage_q8: u32,
179) where
180    D: DrawTarget<Color = Rgb565> + ReadPixel,
181    <D as DrawTarget>::Error: Debug,
182{
183    if x < 0 || y < 0 || x >= width as i32 || coverage_q8 == 0 {
184        return;
185    }
186    let idx = y as usize * width + x as usize;
187    if idx >= zbuffer.len() {
188        return;
189    }
190    let z_depth = crate::to_zdepth(z);
191    if z_depth >= zbuffer[idx].saturating_add(crate::DEPTH_EPSILON) {
192        return;
193    }
194
195    let pixel_was_virgin = zbuffer[idx] == crate::Z_MAX_VALUE;
196    let final_color = if coverage_q8 >= 256 || !pixel_was_virgin {
197        color
198    } else {
199        let bg = fb.read_pixel(Point::new(x, y));
200        blend_q8(bg, color, coverage_q8)
201    };
202    zbuffer[idx] = z_depth;
203    fb.draw_iter([embedded_graphics_core::Pixel(Point::new(x, y), final_color)])
204        .unwrap();
205}
206
207/// Depth epsilon for Z-buffer comparison to prevent Z-fighting
208///
209/// When triangles are nearly coplanar or edge-on to the camera, floating-point
210/// precision errors can cause depth values to be extremely close, leading to
211/// flickering (Z-fighting). This epsilon provides a small bias that helps
212/// new pixels pass the depth test when they are very close to existing ones.
213///
214/// Value: 128 in 16.16 fixed-point format = 0.00195 in floating-point
215/// Tuned for typical embedded graphics scenarios. Increase if Z-fighting persists,
216/// decrease if you notice incorrect depth ordering on distant objects.
217///
218/// **Tuning guide:**
219/// - More Z-fighting? Increase this value (256, 512, etc.)
220/// - Incorrect depth ordering? Decrease this value (64, 32, etc.)
221/// - Adjust camera near/far planes for better depth precision distribution
222
223/// Configuration for depth-based fog effect
224#[derive(Debug, Clone, Copy)]
225pub struct FogConfig {
226    /// Fog color to blend towards
227    pub color: embedded_graphics_core::pixelcolor::Rgb565,
228    /// Near plane distance (fixed-point 16.16 format)
229    pub near: u32,
230    /// Far plane distance (fixed-point 16.16 format)
231    pub far: u32,
232}
233
234impl FogConfig {
235    /// Create a new fog configuration
236    ///
237    /// # Arguments
238    /// * `color` - The fog color
239    /// * `near` - Near distance (depth values closer than this have no fog)
240    /// * `far` - Far distance (depth values farther than this are fully fogged)
241    pub fn new(color: embedded_graphics_core::pixelcolor::Rgb565, near: f32, far: f32) -> Self {
242        Self {
243            color,
244            near: (near * 65536.0) as u32,
245            far: (far * 65536.0) as u32,
246        }
247    }
248
249    /// Apply fog effect to a color based on depth
250    #[inline]
251    pub fn apply(
252        &self,
253        base_color: embedded_graphics_core::pixelcolor::Rgb565,
254        depth: u32,
255    ) -> embedded_graphics_core::pixelcolor::Rgb565 {
256        // Calculate fog factor: 0.0 at near plane, 1.0 at far plane
257        let fog_factor = if depth <= self.near {
258            0u32
259        } else if depth >= self.far {
260            65536u32 // 1.0 in fixed-point
261        } else {
262            // Linear interpolation: (depth - near) / (far - near)
263            let numerator = (depth - self.near) as u64;
264            let denominator = (self.far - self.near) as u64;
265            ((numerator * 65536) / denominator) as u32
266        };
267
268        // Blend base color with fog color
269        // final_color = base_color * (1 - fog_factor) + fog_color * fog_factor
270        let base_r = base_color.r() as u32;
271        let base_g = base_color.g() as u32;
272        let base_b = base_color.b() as u32;
273
274        let fog_r = self.color.r() as u32;
275        let fog_g = self.color.g() as u32;
276        let fog_b = self.color.b() as u32;
277
278        // fog_factor is in 16.16 fixed-point format
279        let r = ((base_r * (65536 - fog_factor) + fog_r * fog_factor) / 65536) as u8;
280        let g = ((base_g * (65536 - fog_factor) + fog_g * fog_factor) / 65536) as u8;
281        let b = ((base_b * (65536 - fog_factor) + fog_b * fog_factor) / 65536) as u8;
282
283        embedded_graphics_core::pixelcolor::Rgb565::new(r, g, b)
284    }
285}
286
287/// Configuration for ordered dithering effect
288#[derive(Debug, Clone, Copy)]
289pub struct DitherConfig {
290    /// Dithering intensity (0-255, where 0 is no dithering)
291    pub intensity: u8,
292}
293
294impl DitherConfig {
295    /// 4x4 Bayer matrix for ordered dithering
296    /// Values are in range [0, 15] and will be scaled by intensity
297    const BAYER_MATRIX: [[u8; 4]; 4] =
298        [[0, 8, 2, 10], [12, 4, 14, 6], [3, 11, 1, 9], [15, 7, 13, 5]];
299
300    /// Create a new dither configuration
301    pub fn new(intensity: u8) -> Self {
302        Self { intensity }
303    }
304
305    /// Apply dithering effect to a color based on screen position
306    #[inline]
307    pub fn apply(
308        &self,
309        color: embedded_graphics_core::pixelcolor::Rgb565,
310        x: i32,
311        y: i32,
312    ) -> embedded_graphics_core::pixelcolor::Rgb565 {
313        if self.intensity == 0 {
314            return color;
315        }
316
317        // Get threshold from Bayer matrix (tiles every 4x4 pixels)
318        let matrix_x = (x & 3) as usize;
319        let matrix_y = (y & 3) as usize;
320        let threshold = Self::BAYER_MATRIX[matrix_y][matrix_x];
321
322        // Scale threshold by intensity
323        // threshold is 0-15, intensity is 0-255
324        // Combined threshold is in range 0-255
325        let scaled_threshold = ((threshold as u16 * self.intensity as u16) / 15) as u8;
326
327        // Apply threshold to each color channel
328        let r = color.r();
329        let g = color.g();
330        let b = color.b();
331
332        // Add dithering noise (can increase or decrease based on threshold)
333        let r = if r > scaled_threshold {
334            r.saturating_sub(scaled_threshold / 2)
335        } else {
336            r.saturating_add(scaled_threshold / 2)
337        };
338
339        let g = if g > scaled_threshold {
340            g.saturating_sub(scaled_threshold / 2)
341        } else {
342            g.saturating_add(scaled_threshold / 2)
343        };
344
345        let b = if b > scaled_threshold {
346            b.saturating_sub(scaled_threshold / 2)
347        } else {
348            b.saturating_add(scaled_threshold / 2)
349        };
350
351        embedded_graphics_core::pixelcolor::Rgb565::new(r, g, b)
352    }
353}
354
355// Fixed-point 16.16 edge stepper — integer-only replacement for f32 invslope.
356const FP_SHIFT: i64 = 16;
357
358#[inline(always)]
359fn fixed_to_i32(value: i64) -> i32 {
360    if value >= 0 {
361        (value >> FP_SHIFT) as i32
362    } else {
363        -((-value) >> FP_SHIFT) as i32
364    }
365}
366
367struct EdgeStepper {
368    x: i64,
369    step: i64,
370}
371
372impl EdgeStepper {
373    fn new(start: Point, end: Point, y: i32) -> Self {
374        let dy = (end.y - start.y) as i64;
375        let (step, x) = if dy != 0 {
376            let s = (((end.x - start.x) as i64) << FP_SHIFT) / dy;
377            let x = ((start.x as i64) << FP_SHIFT) + s * (y - start.y) as i64;
378            (s, x)
379        } else {
380            (0, (start.x as i64) << FP_SHIFT)
381        };
382        Self { x, step }
383    }
384
385    #[inline(always)]
386    fn current_x(&self) -> i32 {
387        fixed_to_i32(self.x)
388    }
389
390    #[inline(always)]
391    fn advance(&mut self) {
392        self.x += self.step;
393    }
394}
395
396#[inline(always)]
397pub fn fill_triangle<D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>>(
398    p1: Point,
399    p2: Point,
400    p3: Point,
401    color: embedded_graphics_core::pixelcolor::Rgb565,
402    fb: &mut D,
403) where
404    <D as DrawTarget>::Error: Debug,
405{
406    let area = (p2.x - p1.x) * (p3.y - p1.y) - (p2.y - p1.y) * (p3.x - p1.x);
407    if area == 0 {
408        // Degenerate triangle (all points collinear)
409        return;
410    }
411
412    let bounds = fb.bounding_box();
413    let min_x = bounds.top_left.x;
414    let max_x = bounds.bottom_right().unwrap().x;
415
416    let mut pixel_row: [embedded_graphics_core::Pixel<embedded_graphics_core::pixelcolor::Rgb565>;
417        MAX_ROW_WIDTH] = [embedded_graphics_core::Pixel(
418        Point::new(0, 0),
419        embedded_graphics_core::pixelcolor::RgbColor::BLACK,
420    ); MAX_ROW_WIDTH];
421
422    // Top part (p1 to p2)
423    if p2.y - p1.y > 0 {
424        let mut a = EdgeStepper::new(p1, p2, p1.y);
425        let mut b = EdgeStepper::new(p1, p3, p1.y);
426
427        for y in p1.y..p2.y {
428            let ax = a.current_x();
429            let bx = b.current_x();
430            let (start_x, end_x) = if ax < bx { (ax, bx) } else { (bx, ax) };
431            let start_x = start_x.clamp(min_x, max_x);
432            let end_x = end_x.clamp(min_x, max_x);
433            let mut x = start_x;
434            while x <= end_x {
435                let chunk_end = (x + MAX_ROW_WIDTH as i32 - 1).min(end_x);
436                let mut i = 0usize;
437                for sx in x..=chunk_end {
438                    pixel_row[i] = embedded_graphics_core::Pixel(Point::new(sx, y), color);
439                    i += 1;
440                }
441                fb.draw_iter(pixel_row[..i].iter().copied()).unwrap();
442                x = chunk_end + 1;
443            }
444            a.advance();
445            b.advance();
446        }
447    }
448
449    // Bottom part (p2 to p3)
450    if p3.y - p2.y > 0 {
451        let mut a = EdgeStepper::new(p2, p3, p2.y);
452        let mut b = EdgeStepper::new(p1, p3, p2.y);
453
454        for y in p2.y..=p3.y {
455            let ax = a.current_x();
456            let bx = b.current_x();
457            let (start_x, end_x) = if ax < bx { (ax, bx) } else { (bx, ax) };
458            let start_x = start_x.clamp(min_x, max_x);
459            let end_x = end_x.clamp(min_x, max_x);
460            let mut x = start_x;
461            while x <= end_x {
462                let chunk_end = (x + MAX_ROW_WIDTH as i32 - 1).min(end_x);
463                let mut i = 0usize;
464                for sx in x..=chunk_end {
465                    pixel_row[i] = embedded_graphics_core::Pixel(Point::new(sx, y), color);
466                    i += 1;
467                }
468                fb.draw_iter(pixel_row[..i].iter().copied()).unwrap();
469                x = chunk_end + 1;
470            }
471            a.advance();
472            b.advance();
473        }
474    }
475}
476
477#[allow(dead_code)]
478fn fill_bottom_flat_triangle<D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>>(
479    p1: Point,
480    p2: Point,
481    p3: Point,
482    color: embedded_graphics_core::pixelcolor::Rgb565,
483    fb: &mut D,
484) where
485    <D as DrawTarget>::Error: Debug,
486{
487    let mut edge1 = EdgeStepper::new(p1, p2, p1.y);
488    let mut edge2 = EdgeStepper::new(p1, p3, p1.y);
489
490    for scanline_y in p1.y..=p2.y {
491        draw_horizontal_line(
492            Point::new(edge1.current_x(), scanline_y),
493            Point::new(edge2.current_x(), scanline_y),
494            color,
495            fb,
496        );
497        edge1.advance();
498        edge2.advance();
499    }
500}
501
502#[allow(dead_code)]
503fn fill_top_flat_triangle<D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>>(
504    p1: Point,
505    p2: Point,
506    p3: Point,
507    color: embedded_graphics_core::pixelcolor::Rgb565,
508    fb: &mut D,
509) where
510    <D as DrawTarget>::Error: Debug,
511{
512    // p1.y == p2.y (top flat), p3 is the bottom vertex; iterate top-down.
513    let mut edge1 = EdgeStepper::new(p1, p3, p1.y);
514    let mut edge2 = EdgeStepper::new(p2, p3, p1.y);
515
516    for scanline_y in p1.y..=p3.y {
517        draw_horizontal_line(
518            Point::new(edge1.current_x(), scanline_y),
519            Point::new(edge2.current_x(), scanline_y),
520            color,
521            fb,
522        );
523        edge1.advance();
524        edge2.advance();
525    }
526}
527
528#[allow(dead_code)]
529fn draw_horizontal_line<D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>>(
530    p1: Point,
531    p2: Point,
532    color: embedded_graphics_core::pixelcolor::Rgb565,
533    fb: &mut D,
534) where
535    <D as DrawTarget>::Error: Debug,
536{
537    let start = p1.x.min(p2.x);
538    let end = p1.x.max(p2.x);
539
540    for x in start..=end {
541        fb.draw_iter([embedded_graphics_core::Pixel(Point::new(x, p1.y), color)])
542            .unwrap();
543    }
544}
545
546#[derive(Clone, Copy, Default)]
547struct ScreenVert {
548    x: f32,
549    y: f32,
550}
551
552#[inline]
553fn clip_polygon_plane_2d(
554    input: &[ScreenVert],
555    output: &mut [ScreenVert; 8],
556    dist: impl Fn(ScreenVert) -> f32,
557) -> usize {
558    let n = input.len();
559    let mut m = 0usize;
560    for i in 0..n {
561        let prev = input[(n + i - 1) % n];
562        let curr = input[i];
563        let d_prev = dist(prev);
564        let d_curr = dist(curr);
565        if d_curr >= 0.0 {
566            if d_prev < 0.0 {
567                let t = d_prev / (d_prev - d_curr);
568                if m < 8 {
569                    output[m] = ScreenVert {
570                        x: prev.x + (curr.x - prev.x) * t,
571                        y: prev.y + (curr.y - prev.y) * t,
572                    };
573                    m += 1;
574                }
575            }
576            if m < 8 {
577                output[m] = curr;
578                m += 1;
579            }
580        } else if d_prev >= 0.0 {
581            let t = d_prev / (d_prev - d_curr);
582            if m < 8 {
583                output[m] = ScreenVert {
584                    x: prev.x + (curr.x - prev.x) * t,
585                    y: prev.y + (curr.y - prev.y) * t,
586                };
587                m += 1;
588            }
589        }
590    }
591    m
592}
593
594#[inline]
595fn tri_area2(a: Point, b: Point, c: Point) -> i32 {
596    (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)
597}
598
599#[inline]
600fn round_to_i32(v: f32) -> i32 {
601    if v >= 0.0 {
602        (v + 0.5) as i32
603    } else {
604        (v - 0.5) as i32
605    }
606}
607
608#[inline]
609fn fill_triangle_screen_clipped<D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>>(
610    p1: Point,
611    p2: Point,
612    p3: Point,
613    color: embedded_graphics_core::pixelcolor::Rgb565,
614    fb: &mut D,
615) where
616    <D as DrawTarget>::Error: Debug,
617{
618    let bounds = fb.bounding_box();
619    let max_x = bounds.size.width.saturating_sub(1) as f32;
620    let max_y = bounds.size.height.saturating_sub(1) as f32;
621    if max_x < 0.0 || max_y < 0.0 {
622        return;
623    }
624
625    let mut a = [ScreenVert::default(); 8];
626    let mut b = [ScreenVert::default(); 8];
627    a[0] = ScreenVert {
628        x: p1.x as f32,
629        y: p1.y as f32,
630    };
631    a[1] = ScreenVert {
632        x: p2.x as f32,
633        y: p2.y as f32,
634    };
635    a[2] = ScreenVert {
636        x: p3.x as f32,
637        y: p3.y as f32,
638    };
639
640    let n = clip_polygon_plane_2d(&a[..3], &mut b, |v| v.x); // x >= 0
641    if n < 3 {
642        return;
643    }
644    let n = clip_polygon_plane_2d(&b[..n], &mut a, |v| max_x - v.x); // x <= max_x
645    if n < 3 {
646        return;
647    }
648    let n = clip_polygon_plane_2d(&a[..n], &mut b, |v| v.y); // y >= 0
649    if n < 3 {
650        return;
651    }
652    let n = clip_polygon_plane_2d(&b[..n], &mut a, |v| max_y - v.y); // y <= max_y
653    if n < 3 {
654        return;
655    }
656
657    for i in 1..n - 1 {
658        let t0 = Point::new(round_to_i32(a[0].x), round_to_i32(a[0].y));
659        let t1 = Point::new(round_to_i32(a[i].x), round_to_i32(a[i].y));
660        let t2 = Point::new(round_to_i32(a[i + 1].x), round_to_i32(a[i + 1].y));
661        if tri_area2(t0, t1, t2) == 0 {
662            continue;
663        }
664        fill_triangle(t0, t1, t2, color, fb);
665    }
666}
667
668#[inline]
669pub fn draw<D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>>(
670    primitive: DrawPrimitive,
671    fb: &mut D,
672) where
673    <D as DrawTarget>::Error: Debug,
674{
675    match primitive {
676        DrawPrimitive::Line([p1, p2], color) => {
677            fb.draw_iter(
678                line_drawing::Bresenham::new((p1.x, p1.y), (p2.x, p2.y))
679                    .map(|(x, y)| embedded_graphics_core::Pixel(Point::new(x, y), color)),
680            )
681            .unwrap();
682        }
683        DrawPrimitive::ColoredPoint(p, c) => {
684            let p = embedded_graphics_core::geometry::Point::new(p.x, p.y);
685
686            fb.draw_iter([embedded_graphics_core::Pixel(p, c)]).unwrap();
687        }
688        DrawPrimitive::ColoredTriangle(mut vertices, color) => {
689            // sort vertices by y using sort_unstable_by
690            vertices.as_mut_slice().sort_unstable_by_key(|a| a.y);
691
692            let [p1, p2, p3] = [
693                Point::new(vertices[0].x, vertices[0].y),
694                Point::new(vertices[1].x, vertices[1].y),
695                Point::new(vertices[2].x, vertices[2].y),
696            ];
697            fill_triangle_screen_clipped(p1, p2, p3, color, fb);
698        }
699        DrawPrimitive::ColoredTriangleWithDepth {
700            points,
701            depths: _,
702            color,
703        }
704        | DrawPrimitive::TranslucentTriangleWithDepth {
705            points,
706            depths: _,
707            color,
708            alpha: _,
709        } => {
710            // This variant should use draw_zbuffered() instead
711            // For compatibility, render without Z-buffering (ignoring depths)
712            let mut vertices = points;
713            if vertices[0].y > vertices[1].y {
714                vertices.swap(0, 1);
715            }
716            if vertices[0].y > vertices[2].y {
717                vertices.swap(0, 2);
718            }
719            if vertices[1].y > vertices[2].y {
720                vertices.swap(1, 2);
721            }
722
723            let mut buf: Vec<_, 3> = Vec::new();
724            for p in vertices.iter() {
725                buf.push(embedded_graphics_core::geometry::Point::new(p.x, p.y))
726                    .unwrap();
727            }
728            let [p1, p2, p3] = buf.into_array().unwrap();
729            fill_triangle_screen_clipped(p1, p2, p3, color, fb);
730        }
731        #[cfg(feature = "lighting")]
732        DrawPrimitive::GouraudTriangle {
733            mut points,
734            mut colors,
735        } => {
736            // Sort vertices by y coordinate (and corresponding colors)
737            if points[0].y > points[1].y {
738                points.swap(0, 1);
739                colors.swap(0, 1);
740            }
741            if points[0].y > points[2].y {
742                points.swap(0, 2);
743                colors.swap(0, 2);
744            }
745            if points[1].y > points[2].y {
746                points.swap(1, 2);
747                colors.swap(1, 2);
748            }
749
750            let mut buf: Vec<_, 3> = Vec::new();
751            for p in points.iter() {
752                buf.push(embedded_graphics_core::geometry::Point::new(p.x, p.y))
753                    .unwrap();
754            }
755            let [p1, p2, p3] = buf.into_array().unwrap();
756            let [c1, c2, c3] = colors;
757
758            // Off-screen culling.
759            let bounds = fb.bounding_box();
760            let scr_w = bounds.size.width as i32;
761            let scr_h = bounds.size.height as i32;
762            if p1.x < 0 && p2.x < 0 && p3.x < 0 {
763                return;
764            }
765            if p1.x >= scr_w && p2.x >= scr_w && p3.x >= scr_w {
766                return;
767            }
768            if p1.y < 0 && p2.y < 0 && p3.y < 0 {
769                return;
770            }
771            if p1.y >= scr_h && p2.y >= scr_h && p3.y >= scr_h {
772                return;
773            }
774
775            if p2.y == p3.y {
776                fill_bottom_flat_gouraud(p1, p2, p3, c1, c2, c3, fb);
777            } else if p1.y == p2.y {
778                fill_top_flat_gouraud(p1, p2, p3, c1, c2, c3, fb);
779            } else {
780                // Split triangle into two flat triangles
781                let t = (p2.y - p1.y) as f32 / (p3.y - p1.y) as f32;
782                let p4 = Point::new((p1.x as f32 + t * (p3.x - p1.x) as f32) as i32, p2.y);
783                // Interpolate color at split point
784                let c4 = interpolate_color(c1, c3, t);
785
786                fill_bottom_flat_gouraud(p1, p2, p4, c1, c2, c4, fb);
787                fill_top_flat_gouraud(p2, p4, p3, c2, c4, c3, fb);
788            }
789        }
790        #[cfg(feature = "lighting")]
791        DrawPrimitive::GouraudTriangleWithDepth {
792            points,
793            depths: _,
794            colors,
795        } => {
796            // This variant should use draw_zbuffered() instead
797            // For compatibility, render without Z-buffering (ignoring depths)
798            let prim = DrawPrimitive::GouraudTriangle { points, colors };
799            draw(prim, fb);
800        }
801        #[cfg(feature = "textured")]
802        DrawPrimitive::TexturedTriangle { .. }
803        | DrawPrimitive::TexturedTriangleWithDepth { .. }
804        | DrawPrimitive::TexturedGouraudTriangleWithDepth { .. }
805        | DrawPrimitive::LightmappedTriangle { .. } => {
806            // Textured / lightmapped triangles require a TextureManager.
807            // Use draw_zbuffered_with_textures() / draw_zbuffered_lightmapped() instead.
808        }
809    }
810}
811
812#[cfg(feature = "lighting")]
813// Interpolate between two colors
814#[inline]
815fn interpolate_color(
816    c1: embedded_graphics_core::pixelcolor::Rgb565,
817    c2: embedded_graphics_core::pixelcolor::Rgb565,
818    t: f32,
819) -> embedded_graphics_core::pixelcolor::Rgb565 {
820    let r1 = c1.r() as f32;
821    let g1 = c1.g() as f32;
822    let b1 = c1.b() as f32;
823
824    let r2 = c2.r() as f32;
825    let g2 = c2.g() as f32;
826    let b2 = c2.b() as f32;
827
828    let r = (r1 + t * (r2 - r1)) as u8;
829    let g = (g1 + t * (g2 - g1)) as u8;
830    let b = (b1 + t * (b2 - b1)) as u8;
831
832    embedded_graphics_core::pixelcolor::Rgb565::new(r, g, b)
833}
834
835#[cfg(feature = "lighting")]
836// Gouraud shading - bottom flat triangle with color interpolation
837fn fill_bottom_flat_gouraud<D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>>(
838    p1: Point,
839    p2: Point,
840    p3: Point,
841    c1: embedded_graphics_core::pixelcolor::Rgb565,
842    c2: embedded_graphics_core::pixelcolor::Rgb565,
843    c3: embedded_graphics_core::pixelcolor::Rgb565,
844    fb: &mut D,
845) where
846    <D as DrawTarget>::Error: Debug,
847{
848    let height = (p2.y - p1.y) as f32;
849    if height == 0.0 {
850        return;
851    }
852
853    let mut edge1 = EdgeStepper::new(p1, p2, p1.y);
854    let mut edge2 = EdgeStepper::new(p1, p3, p1.y);
855
856    for scanline_y in p1.y..=p2.y {
857        let t = (scanline_y - p1.y) as f32 / height;
858        let color_left = interpolate_color(c1, c2, t);
859        let color_right = interpolate_color(c1, c3, t);
860
861        draw_horizontal_line_gouraud(
862            Point::new(edge1.current_x(), scanline_y),
863            Point::new(edge2.current_x(), scanline_y),
864            color_left,
865            color_right,
866            fb,
867        );
868
869        edge1.advance();
870        edge2.advance();
871    }
872}
873
874#[cfg(feature = "lighting")]
875// Gouraud shading - top flat triangle with color interpolation
876fn fill_top_flat_gouraud<D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>>(
877    p1: Point,
878    p2: Point,
879    p3: Point,
880    c1: embedded_graphics_core::pixelcolor::Rgb565,
881    c2: embedded_graphics_core::pixelcolor::Rgb565,
882    c3: embedded_graphics_core::pixelcolor::Rgb565,
883    fb: &mut D,
884) where
885    <D as DrawTarget>::Error: Debug,
886{
887    // p1.y == p2.y (top flat), p3 is the bottom vertex; iterate top-down.
888    let height = (p3.y - p1.y) as f32;
889    if height == 0.0 {
890        return;
891    }
892
893    let mut edge1 = EdgeStepper::new(p1, p3, p1.y);
894    let mut edge2 = EdgeStepper::new(p2, p3, p1.y);
895
896    for scanline_y in p1.y..=p3.y {
897        let t = (scanline_y - p1.y) as f32 / height;
898        let color_left = interpolate_color(c1, c3, t);
899        let color_right = interpolate_color(c2, c3, t);
900
901        draw_horizontal_line_gouraud(
902            Point::new(edge1.current_x(), scanline_y),
903            Point::new(edge2.current_x(), scanline_y),
904            color_left,
905            color_right,
906            fb,
907        );
908
909        edge1.advance();
910        edge2.advance();
911    }
912}
913
914#[cfg(feature = "lighting")]
915// Draw a horizontal line with color interpolation (Gouraud)
916fn draw_horizontal_line_gouraud<D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>>(
917    p1: Point,
918    p2: Point,
919    color1: embedded_graphics_core::pixelcolor::Rgb565,
920    color2: embedded_graphics_core::pixelcolor::Rgb565,
921    fb: &mut D,
922) where
923    <D as DrawTarget>::Error: Debug,
924{
925    let start = p1.x.min(p2.x);
926    let end = p1.x.max(p2.x);
927    let width = (end - start) as f32;
928
929    if width == 0.0 {
930        fb.draw_iter([embedded_graphics_core::Pixel(
931            Point::new(start, p1.y),
932            color1,
933        )])
934        .unwrap();
935        return;
936    }
937
938    for x in start..=end {
939        let t = (x - start) as f32 / width;
940        let color = interpolate_color(color1, color2, t);
941        fb.draw_iter([embedded_graphics_core::Pixel(Point::new(x, p1.y), color)])
942            .unwrap();
943    }
944}
945
946// Z-buffered drawing function
947// Using u32 for Z-buffer is much faster on embedded systems without FPU
948#[inline]
949pub fn draw_zbuffered<D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>>(
950    primitive: DrawPrimitive,
951    fb: &mut D,
952    zbuffer: &mut [crate::ZDepth],
953    width: usize,
954) where
955    <D as DrawTarget>::Error: Debug,
956{
957    // Call with no effects for backward compatibility
958    draw_zbuffered_with_effects(primitive, fb, zbuffer, width, None, None);
959}
960
961/// Z-buffered drawing with analytical edge anti-aliasing.
962///
963/// Renders triangles with sub-pixel-accurate left/right edge coverage by
964/// blending the boundary pixels with the existing framebuffer contents.
965/// Inner pixels of each scanline use the same fast path as `draw_zbuffered`.
966/// Lines use Wu's algorithm.
967///
968/// Requires `PixelRead` on the framebuffer for the boundary blends.
969#[cfg(feature = "aa-heuristic")]
970#[inline]
971pub fn draw_zbuffered_aa<D>(
972    primitive: DrawPrimitive,
973    fb: &mut D,
974    zbuffer: &mut [crate::ZDepth],
975    width: usize,
976) where
977    D: DrawTarget<Color = Rgb565> + ReadPixel,
978    <D as DrawTarget>::Error: Debug,
979{
980    match primitive {
981        DrawPrimitive::ColoredTriangleWithDepth {
982            mut points,
983            mut depths,
984            color,
985        } => {
986            if points[0].y > points[1].y {
987                points.swap(0, 1);
988                depths.swap(0, 1);
989            }
990            if points[0].y > points[2].y {
991                points.swap(0, 2);
992                depths.swap(0, 2);
993            }
994            if points[1].y > points[2].y {
995                points.swap(1, 2);
996                depths.swap(1, 2);
997            }
998            let [p1, p2, p3] = points;
999            let [z1, z2, z3] = depths;
1000
1001            // Off-screen culling.
1002            let scr_w = width as i32;
1003            let scr_h = (zbuffer.len() / width) as i32;
1004            if p1.x < 0 && p2.x < 0 && p3.x < 0 {
1005                return;
1006            }
1007            if p1.x >= scr_w && p2.x >= scr_w && p3.x >= scr_w {
1008                return;
1009            }
1010            if p1.y < 0 && p2.y < 0 && p3.y < 0 {
1011                return;
1012            }
1013            if p1.y >= scr_h && p2.y >= scr_h && p3.y >= scr_h {
1014                return;
1015            }
1016
1017            fill_triangle_zbuffered_aa(p1, p2, p3, z1, z2, z3, color, fb, zbuffer, width);
1018        }
1019        DrawPrimitive::Line([p1, p2], color) => {
1020            draw_line_aa(p1.x, p1.y, p2.x, p2.y, color, fb);
1021        }
1022        // Anything else (Gouraud, textured, points) — fall back to the
1023        // non-AA path. AA variants for those can be added as needed.
1024        other => draw_zbuffered(other, fb, zbuffer, width),
1025    }
1026}
1027
1028/// Z-buffered drawing with 2xSSAA (Super-Sampling Anti-Aliasing) sub-pixel edge anti-aliasing.
1029#[cfg(feature = "aa")]
1030#[inline]
1031pub fn draw_zbuffered_2xssaa<D>(
1032    primitive: DrawPrimitive,
1033    fb: &mut D,
1034    zbuffer: &mut [crate::ZDepth],
1035    width: usize,
1036) where
1037    D: DrawTarget<Color = Rgb565> + ReadPixel,
1038    <D as DrawTarget>::Error: Debug,
1039{
1040    match primitive {
1041        DrawPrimitive::ColoredTriangleWithDepth {
1042            mut points,
1043            mut depths,
1044            color,
1045        } => {
1046            if points[0].y > points[1].y {
1047                points.swap(0, 1);
1048                depths.swap(0, 1);
1049            }
1050            if points[0].y > points[2].y {
1051                points.swap(0, 2);
1052                depths.swap(0, 2);
1053            }
1054            if points[1].y > points[2].y {
1055                points.swap(1, 2);
1056                depths.swap(1, 2);
1057            }
1058            let [p1, p2, p3] = points;
1059            let [z1, z2, z3] = depths;
1060
1061            let scr_w = width as i32;
1062            let scr_h = (zbuffer.len() / width) as i32;
1063            if p1.x < 0 && p2.x < 0 && p3.x < 0 {
1064                return;
1065            }
1066            if p1.x >= scr_w && p2.x >= scr_w && p3.x >= scr_w {
1067                return;
1068            }
1069            if p1.y < 0 && p2.y < 0 && p3.y < 0 {
1070                return;
1071            }
1072            if p1.y >= scr_h && p2.y >= scr_h && p3.y >= scr_h {
1073                return;
1074            }
1075
1076            fill_triangle_zbuffered_2xssaa(p1, p2, p3, z1, z2, z3, color, fb, zbuffer, width);
1077        }
1078        DrawPrimitive::Line([p1, p2], color) => {
1079            draw_line_aa(p1.x, p1.y, p2.x, p2.y, color, fb);
1080        }
1081        other => draw_zbuffered(other, fb, zbuffer, width),
1082    }
1083}
1084
1085#[cfg(feature = "aa")]
1086#[inline(always)]
1087fn fill_triangle_zbuffered_2xssaa<D>(
1088    p1: nalgebra::Point2<i32>,
1089    p2: nalgebra::Point2<i32>,
1090    p3: nalgebra::Point2<i32>,
1091    z1: f32,
1092    z2: f32,
1093    z3: f32,
1094    color: Rgb565,
1095    fb: &mut D,
1096    zbuffer: &mut [crate::ZDepth],
1097    width: usize,
1098) where
1099    D: DrawTarget<Color = Rgb565> + ReadPixel,
1100    <D as DrawTarget>::Error: Debug,
1101{
1102    let p1_eg = Point::new(p1.x, p1.y);
1103    let p2_eg = Point::new(p2.x, p2.y);
1104    let p3_eg = Point::new(p3.x, p3.y);
1105
1106    let z1_int = (z1 * 65536.0) as u32;
1107    let z2_int = (z2 * 65536.0) as u32;
1108    let z3_int = (z3 * 65536.0) as u32;
1109
1110    if p2_eg.y == p3_eg.y {
1111        fill_bottom_flat_2xssaa(
1112            p1_eg, p2_eg, p3_eg, z1_int, z2_int, z3_int, color, fb, zbuffer, width,
1113        );
1114    } else if p1_eg.y == p2_eg.y {
1115        fill_top_flat_2xssaa(
1116            p1_eg, p2_eg, p3_eg, z1_int, z2_int, z3_int, color, fb, zbuffer, width,
1117        );
1118    } else {
1119        let t = (p2_eg.y - p1_eg.y) as f32 / (p3_eg.y - p1_eg.y) as f32;
1120        let p4 = Point::new(
1121            (p1_eg.x as f32 + t * (p3_eg.x - p1_eg.x) as f32) as i32,
1122            p2_eg.y,
1123        );
1124        let z4_int = (z1_int as i64 + (t * (z3_int as i64 - z1_int as i64) as f32) as i64) as u32;
1125        fill_bottom_flat_2xssaa(
1126            p1_eg, p2_eg, p4, z1_int, z2_int, z4_int, color, fb, zbuffer, width,
1127        );
1128        fill_top_flat_2xssaa(
1129            p2_eg, p4, p3_eg, z2_int, z4_int, z3_int, color, fb, zbuffer, width,
1130        );
1131    }
1132}
1133
1134#[cfg(feature = "aa")]
1135#[inline(always)]
1136fn fill_bottom_flat_2xssaa<D>(
1137    p1: Point,
1138    p2: Point,
1139    p3: Point,
1140    z1: u32,
1141    z2: u32,
1142    z3: u32,
1143    color: Rgb565,
1144    fb: &mut D,
1145    zbuffer: &mut [crate::ZDepth],
1146    width: usize,
1147) where
1148    D: DrawTarget<Color = Rgb565> + ReadPixel,
1149    <D as DrawTarget>::Error: Debug,
1150{
1151    let height = p2.y - p1.y;
1152    if height == 0 {
1153        return;
1154    }
1155    let invslope1 = ((p2.x - p1.x) << 16) / height;
1156    let invslope2 = ((p3.x - p1.x) << 16) / height;
1157
1158    let mut curx1 = p1.x << 16;
1159    let mut curx2 = p1.x << 16;
1160
1161    for scanline_y in p1.y..=p2.y {
1162        let dy = scanline_y - p1.y;
1163        let z_left = (z1 as i64 + ((z2 as i64 - z1 as i64) * dy as i64 / height as i64)) as u32;
1164        let z_right = (z1 as i64 + ((z3 as i64 - z1 as i64) * dy as i64 / height as i64)) as u32;
1165
1166        ssaa2x_scanline(
1167            curx1, curx2, scanline_y, z_left, z_right, color, fb, zbuffer, width,
1168        );
1169
1170        curx1 += invslope1;
1171        curx2 += invslope2;
1172    }
1173}
1174
1175#[cfg(feature = "aa")]
1176#[inline(always)]
1177fn fill_top_flat_2xssaa<D>(
1178    p1: Point,
1179    p2: Point,
1180    p3: Point,
1181    z1: u32,
1182    z2: u32,
1183    z3: u32,
1184    color: Rgb565,
1185    fb: &mut D,
1186    zbuffer: &mut [crate::ZDepth],
1187    width: usize,
1188) where
1189    D: DrawTarget<Color = Rgb565> + ReadPixel,
1190    <D as DrawTarget>::Error: Debug,
1191{
1192    let height = p3.y - p1.y;
1193    if height == 0 {
1194        return;
1195    }
1196    let invslope1 = ((p3.x - p1.x) << 16) / height;
1197    let invslope2 = ((p3.x - p2.x) << 16) / height;
1198
1199    let mut curx1 = p3.x << 16;
1200    let mut curx2 = p3.x << 16;
1201
1202    for scanline_y in (p1.y..=p3.y).rev() {
1203        let dy = scanline_y - p1.y;
1204        let z_left = (z1 as i64 + ((z3 as i64 - z1 as i64) * dy as i64 / height as i64)) as u32;
1205        let z_right = (z2 as i64 + ((z3 as i64 - z2 as i64) * dy as i64 / height as i64)) as u32;
1206
1207        ssaa2x_scanline(
1208            curx1, curx2, scanline_y, z_left, z_right, color, fb, zbuffer, width,
1209        );
1210
1211        curx1 -= invslope1;
1212        curx2 -= invslope2;
1213    }
1214}
1215
1216#[cfg(feature = "aa")]
1217#[inline(always)]
1218fn ssaa2x_scanline<D>(
1219    cx1: i32,
1220    cx2: i32,
1221    y: i32,
1222    z_left: u32,
1223    z_right: u32,
1224    color: Rgb565,
1225    fb: &mut D,
1226    zbuffer: &mut [crate::ZDepth],
1227    width: usize,
1228) where
1229    D: DrawTarget<Color = Rgb565> + ReadPixel,
1230    <D as DrawTarget>::Error: Debug,
1231{
1232    let (left_fx, right_fx, z_l, z_r) = if cx1 <= cx2 {
1233        (cx1, cx2, z_left, z_right)
1234    } else {
1235        (cx2, cx1, z_right, z_left)
1236    };
1237
1238    let l_int = left_fx >> 16;
1239    let r_int = right_fx >> 16;
1240    let span = r_int - l_int;
1241
1242    let eval_subsamples = |x: i32| -> u32 {
1243        let fx1 = (x << 16) | 16384;
1244        let fx2 = (x << 16) | 49152;
1245        let s1 = (fx1 >= left_fx && fx1 <= right_fx) as u32;
1246        let s2 = (fx2 >= left_fx && fx2 <= right_fx) as u32;
1247        s1 + s2
1248    };
1249
1250    if l_int == r_int {
1251        let samples = eval_subsamples(l_int);
1252        if samples > 0 {
1253            let cov_q8 = samples * 128;
1254            aa_pixel(fb, l_int, y, color, z_l, zbuffer, width, cov_q8.min(256));
1255        }
1256        return;
1257    }
1258
1259    let left_samples = eval_subsamples(l_int);
1260    if left_samples > 0 {
1261        let cov_q8 = left_samples * 128;
1262        aa_pixel(fb, l_int, y, color, z_l, zbuffer, width, cov_q8.min(256));
1263    }
1264
1265    if span > 1 {
1266        for x in (l_int + 1)..r_int {
1267            let t = (x - l_int) as f32 / span as f32;
1268            let z = (z_l as f32 + t * (z_r as f32 - z_l as f32)) as u32;
1269            aa_pixel(fb, x, y, color, z, zbuffer, width, 256);
1270        }
1271    }
1272
1273    let right_samples = eval_subsamples(r_int);
1274    if right_samples > 0 {
1275        let cov_q8 = right_samples * 128;
1276        aa_pixel(fb, r_int, y, color, z_r, zbuffer, width, cov_q8.min(256));
1277    }
1278}
1279
1280#[cfg(feature = "aa-heuristic")]
1281#[inline(always)]
1282fn fill_triangle_zbuffered_aa<D>(
1283    p1: nalgebra::Point2<i32>,
1284    p2: nalgebra::Point2<i32>,
1285    p3: nalgebra::Point2<i32>,
1286    z1: f32,
1287    z2: f32,
1288    z3: f32,
1289    color: Rgb565,
1290    fb: &mut D,
1291    zbuffer: &mut [crate::ZDepth],
1292    width: usize,
1293) where
1294    D: DrawTarget<Color = Rgb565> + ReadPixel,
1295    <D as DrawTarget>::Error: Debug,
1296{
1297    let p1_eg = Point::new(p1.x, p1.y);
1298    let p2_eg = Point::new(p2.x, p2.y);
1299    let p3_eg = Point::new(p3.x, p3.y);
1300
1301    let z1_int = (z1 * 65536.0) as u32;
1302    let z2_int = (z2 * 65536.0) as u32;
1303    let z3_int = (z3 * 65536.0) as u32;
1304
1305    if p2_eg.y == p3_eg.y {
1306        fill_bottom_flat_aa(
1307            p1_eg, p2_eg, p3_eg, z1_int, z2_int, z3_int, color, fb, zbuffer, width,
1308        );
1309    } else if p1_eg.y == p2_eg.y {
1310        fill_top_flat_aa(
1311            p1_eg, p2_eg, p3_eg, z1_int, z2_int, z3_int, color, fb, zbuffer, width,
1312        );
1313    } else {
1314        let t = (p2_eg.y - p1_eg.y) as f32 / (p3_eg.y - p1_eg.y) as f32;
1315        let p4 = Point::new(
1316            (p1_eg.x as f32 + t * (p3_eg.x - p1_eg.x) as f32) as i32,
1317            p2_eg.y,
1318        );
1319        let z4_int = (z1_int as i64 + (t * (z3_int as i64 - z1_int as i64) as f32) as i64) as u32;
1320        fill_bottom_flat_aa(
1321            p1_eg, p2_eg, p4, z1_int, z2_int, z4_int, color, fb, zbuffer, width,
1322        );
1323        fill_top_flat_aa(
1324            p2_eg, p4, p3_eg, z2_int, z4_int, z3_int, color, fb, zbuffer, width,
1325        );
1326    }
1327}
1328
1329#[cfg(feature = "aa-heuristic")]
1330#[inline(always)]
1331fn fill_bottom_flat_aa<D>(
1332    p1: Point,
1333    p2: Point,
1334    p3: Point,
1335    z1: u32,
1336    z2: u32,
1337    z3: u32,
1338    color: Rgb565,
1339    fb: &mut D,
1340    zbuffer: &mut [crate::ZDepth],
1341    width: usize,
1342) where
1343    D: DrawTarget<Color = Rgb565> + ReadPixel,
1344    <D as DrawTarget>::Error: Debug,
1345{
1346    let height = p2.y - p1.y;
1347    if height == 0 {
1348        return;
1349    }
1350    let invslope1 = ((p2.x - p1.x) << 16) / height;
1351    let invslope2 = ((p3.x - p1.x) << 16) / height;
1352
1353    let mut curx1 = p1.x << 16;
1354    let mut curx2 = p1.x << 16;
1355
1356    for scanline_y in p1.y..=p2.y {
1357        let dy = scanline_y - p1.y;
1358        let z_left = (z1 as i64 + ((z2 as i64 - z1 as i64) * dy as i64 / height as i64)) as u32;
1359        let z_right = (z1 as i64 + ((z3 as i64 - z1 as i64) * dy as i64 / height as i64)) as u32;
1360
1361        aa_scanline(
1362            curx1, curx2, scanline_y, z_left, z_right, color, fb, zbuffer, width,
1363        );
1364
1365        curx1 += invslope1;
1366        curx2 += invslope2;
1367    }
1368}
1369
1370#[cfg(feature = "aa-heuristic")]
1371#[inline(always)]
1372fn fill_top_flat_aa<D>(
1373    p1: Point,
1374    p2: Point,
1375    p3: Point,
1376    z1: u32,
1377    z2: u32,
1378    z3: u32,
1379    color: Rgb565,
1380    fb: &mut D,
1381    zbuffer: &mut [crate::ZDepth],
1382    width: usize,
1383) where
1384    D: DrawTarget<Color = Rgb565> + ReadPixel,
1385    <D as DrawTarget>::Error: Debug,
1386{
1387    let height = p3.y - p1.y;
1388    if height == 0 {
1389        return;
1390    }
1391    let invslope1 = ((p3.x - p1.x) << 16) / height;
1392    let invslope2 = ((p3.x - p2.x) << 16) / height;
1393
1394    let mut curx1 = p3.x << 16;
1395    let mut curx2 = p3.x << 16;
1396
1397    for scanline_y in (p1.y..=p3.y).rev() {
1398        let dy = scanline_y - p1.y;
1399        let z_left = (z1 as i64 + ((z3 as i64 - z1 as i64) * dy as i64 / height as i64)) as u32;
1400        let z_right = (z2 as i64 + ((z3 as i64 - z2 as i64) * dy as i64 / height as i64)) as u32;
1401
1402        aa_scanline(
1403            curx1, curx2, scanline_y, z_left, z_right, color, fb, zbuffer, width,
1404        );
1405
1406        curx1 -= invslope1;
1407        curx2 -= invslope2;
1408    }
1409}
1410
1411/// Render one scanline with analytical left/right edge coverage.
1412///
1413/// `cx1` / `cx2` are 16.16 fixed-point edge positions. The fractional parts
1414/// give us per-edge sub-pixel coverage; the integer span between them is
1415/// rendered with the existing fully-opaque fast path.
1416#[cfg(feature = "aa-heuristic")]
1417#[inline(always)]
1418fn aa_scanline<D>(
1419    cx1: i32,
1420    cx2: i32,
1421    y: i32,
1422    z_left: u32,
1423    z_right: u32,
1424    color: Rgb565,
1425    fb: &mut D,
1426    zbuffer: &mut [crate::ZDepth],
1427    width: usize,
1428) where
1429    D: DrawTarget<Color = Rgb565> + ReadPixel,
1430    <D as DrawTarget>::Error: Debug,
1431{
1432    // Normalize: left should be the smaller fixed-point x.
1433    let (left_fx, right_fx, z_l, z_r) = if cx1 <= cx2 {
1434        (cx1, cx2, z_left, z_right)
1435    } else {
1436        (cx2, cx1, z_right, z_left)
1437    };
1438
1439    let l_int = left_fx >> 16;
1440    let r_int = right_fx >> 16;
1441    let l_frac_q16 = (left_fx & 0xFFFF) as u32;
1442    let r_frac_q16 = (right_fx & 0xFFFF) as u32;
1443
1444    // Linear z interpolation across the inner span.
1445    let span = r_int - l_int;
1446
1447    if l_int == r_int {
1448        // Sub-pixel span: triangle width < 1px on this row. Coverage equals
1449        // the float-difference of the edge positions.
1450        let cov_q16 = r_frac_q16.saturating_sub(l_frac_q16);
1451        aa_pixel(fb, l_int, y, color, z_l, zbuffer, width, cov_q16 >> 8);
1452        return;
1453    }
1454
1455    // Left boundary: covered fraction is (1 - l_frac).
1456    let left_cov_q8 = 256 - (l_frac_q16 >> 8);
1457    aa_pixel(fb, l_int, y, color, z_l, zbuffer, width, left_cov_q8);
1458
1459    // Inner pixels: full coverage. Reuse the existing scanline fast path
1460    // semantics inline (z-test + write, no read-blend).
1461    if span > 1 {
1462        for x in (l_int + 1)..r_int {
1463            if x < 0 {
1464                continue;
1465            }
1466            let idx = y as usize * width + x as usize;
1467            if idx >= zbuffer.len() {
1468                continue;
1469            }
1470            // Linear interp z across the inner pixels
1471            let t_num = (x - l_int) as i64;
1472            let t_den = span as i64;
1473            let z = (z_l as i64 + ((z_r as i64 - z_l as i64) * t_num / t_den)) as u32;
1474            let z_depth = crate::to_zdepth(z);
1475            if z_depth < zbuffer[idx].saturating_add(crate::DEPTH_EPSILON) {
1476                zbuffer[idx] = z_depth;
1477                fb.draw_iter([embedded_graphics_core::Pixel(Point::new(x, y), color)])
1478                    .unwrap();
1479            }
1480        }
1481    }
1482
1483    // Right boundary: covered fraction is r_frac itself.
1484    if r_frac_q16 > 0 {
1485        let right_cov_q8 = r_frac_q16 >> 8;
1486        aa_pixel(fb, r_int, y, color, z_r, zbuffer, width, right_cov_q8);
1487    }
1488}
1489
1490/// Z-buffered drawing with coverage-tracked analytical edge AA.
1491///
1492/// Differs from `draw_zbuffered_aa` by maintaining a per-pixel coverage
1493/// buffer (`u8`, 0..255) so that multiple triangles meeting at a shared
1494/// edge composite additively rather than overwriting each other. This
1495/// eliminates the residual color-step seam at coplanar shared edges that
1496/// the simpler `draw_zbuffered_aa` heuristic leaves behind.
1497///
1498/// **Caller protocol per frame:**
1499/// 1. Clear `zbuffer` to `u32::MAX` and `coverage` to `0`.
1500/// 2. Render all primitives via this function.
1501/// 3. Call [`composite_aa_background`] with the desired background color.
1502///    This blends `bg` into pixels that aren't fully covered.
1503///
1504/// **Cost:** an additional `width * height` byte buffer (~6 KiB at 96×64).
1505/// Inner-pixel rasterization is the same fast path as `draw_zbuffered`.
1506#[cfg(feature = "aa-coverage")]
1507#[inline]
1508pub fn draw_zbuffered_aa_coverage<D>(
1509    primitive: DrawPrimitive,
1510    fb: &mut D,
1511    zbuffer: &mut [crate::ZDepth],
1512    coverage: &mut [u8],
1513    width: usize,
1514) where
1515    D: DrawTarget<Color = Rgb565> + ReadPixel,
1516    <D as DrawTarget>::Error: Debug,
1517{
1518    match primitive {
1519        DrawPrimitive::ColoredTriangleWithDepth {
1520            mut points,
1521            mut depths,
1522            color,
1523        } => {
1524            if points[0].y > points[1].y {
1525                points.swap(0, 1);
1526                depths.swap(0, 1);
1527            }
1528            if points[0].y > points[2].y {
1529                points.swap(0, 2);
1530                depths.swap(0, 2);
1531            }
1532            if points[1].y > points[2].y {
1533                points.swap(1, 2);
1534                depths.swap(1, 2);
1535            }
1536            let [p1, p2, p3] = points;
1537            let [z1, z2, z3] = depths;
1538            fill_triangle_zbuffered_aa_cov(
1539                p1, p2, p3, z1, z2, z3, color, fb, zbuffer, coverage, width,
1540            );
1541        }
1542        DrawPrimitive::Line([p1, p2], color) => {
1543            // Use the coverage-aware Wu's variant so the bg composite at
1544            // end-of-frame doesn't overwrite line pixels.
1545            draw_line_aa_coverage(p1.x, p1.y, p2.x, p2.y, color, fb, coverage, width);
1546        }
1547        other => draw_zbuffered(other, fb, zbuffer, width),
1548    }
1549}
1550
1551/// Wu's anti-aliased line that updates a coverage buffer alongside the
1552/// framebuffer write. Use with `draw_zbuffered_aa_coverage` so the bg
1553/// composite at end-of-frame respects line pixels.
1554#[cfg(feature = "aa-coverage")]
1555pub fn draw_line_aa_coverage<D>(
1556    x0: i32,
1557    y0: i32,
1558    x1: i32,
1559    y1: i32,
1560    color: Rgb565,
1561    fb: &mut D,
1562    coverage: &mut [u8],
1563    width: usize,
1564) where
1565    D: DrawTarget<Color = Rgb565> + ReadPixel,
1566    <D as DrawTarget>::Error: Debug,
1567{
1568    let dx = (x1 - x0).abs();
1569    let dy = (y1 - y0).abs();
1570    let steep = dy > dx;
1571    let (x0, y0, x1, y1) = if steep {
1572        (y0, x0, y1, x1)
1573    } else {
1574        (x0, y0, x1, y1)
1575    };
1576    let (x0, y0, x1, y1) = if x0 > x1 {
1577        (x1, y1, x0, y0)
1578    } else {
1579        (x0, y0, x1, y1)
1580    };
1581    let dx = x1 - x0;
1582    let dy = y1 - y0;
1583    if dx == 0 {
1584        let (px, py) = if steep { (y0, x0) } else { (x0, y0) };
1585        plot_aa_cov(fb, px, py, color, coverage, width, 256);
1586        return;
1587    }
1588    let gradient: i32 = ((dy as i64) << 16) as i32 / dx;
1589    let mut intery: i32 = y0 << 16;
1590    for x in x0..=x1 {
1591        let y_int = intery >> 16;
1592        let frac_q16 = (intery & 0xFFFF) as u32;
1593        let cov_top = 256 - (frac_q16 >> 8);
1594        let cov_bot = frac_q16 >> 8;
1595        if steep {
1596            plot_aa_cov(fb, y_int, x, color, coverage, width, cov_top);
1597            plot_aa_cov(fb, y_int + 1, x, color, coverage, width, cov_bot);
1598        } else {
1599            plot_aa_cov(fb, x, y_int, color, coverage, width, cov_top);
1600            plot_aa_cov(fb, x, y_int + 1, color, coverage, width, cov_bot);
1601        }
1602        intery += gradient;
1603    }
1604}
1605
1606/// Coverage-aware single-pixel plot for Wu's lines. Mirrors the four-case
1607/// logic of `aa_pixel_cov` but without the z-test (lines don't carry depth).
1608#[cfg(feature = "aa-coverage")]
1609#[inline(always)]
1610fn plot_aa_cov<D>(
1611    fb: &mut D,
1612    x: i32,
1613    y: i32,
1614    color: Rgb565,
1615    coverage: &mut [u8],
1616    width: usize,
1617    coverage_q8: u32,
1618) where
1619    D: DrawTarget<Color = Rgb565> + ReadPixel,
1620    <D as DrawTarget>::Error: Debug,
1621{
1622    if x < 0 || y < 0 || x >= width as i32 || coverage_q8 == 0 {
1623        return;
1624    }
1625    let idx = y as usize * width + x as usize;
1626    if idx >= coverage.len() {
1627        return;
1628    }
1629    let p = Point::new(x, y);
1630
1631    if coverage_q8 >= 256 {
1632        coverage[idx] = 255;
1633        fb.draw_iter([embedded_graphics_core::Pixel(p, color)])
1634            .unwrap();
1635        return;
1636    }
1637
1638    let prev_cov = coverage[idx] as u32;
1639
1640    if prev_cov == 0 {
1641        let claim_255 = (coverage_q8 * 255) >> 8;
1642        coverage[idx] = claim_255 as u8;
1643        fb.draw_iter([embedded_graphics_core::Pixel(p, color)])
1644            .unwrap();
1645        return;
1646    }
1647
1648    if prev_cov >= 255 {
1649        let existing = fb.read_pixel(p);
1650        let result = blend_q8(existing, color, coverage_q8);
1651        fb.draw_iter([embedded_graphics_core::Pixel(p, result)])
1652            .unwrap();
1653        return;
1654    }
1655
1656    let remaining = 255 - prev_cov;
1657    let claim_255 = ((coverage_q8 * 255) >> 8).min(remaining);
1658    if claim_255 == 0 {
1659        return;
1660    }
1661    let new_total = prev_cov + claim_255;
1662    let existing = fb.read_pixel(p);
1663    let blend_factor = (claim_255 * 256) / new_total;
1664    let result = blend_q8(existing, color, blend_factor);
1665    coverage[idx] = new_total as u8;
1666    fb.draw_iter([embedded_graphics_core::Pixel(p, result)])
1667        .unwrap();
1668}
1669
1670/// Composite background color into pixels that weren't fully covered by
1671/// the AA rasterizer. Run once per frame after all primitives have been
1672/// drawn via `draw_zbuffered_aa_coverage`.
1673#[cfg(feature = "aa-coverage")]
1674pub fn composite_aa_background<D>(
1675    fb: &mut D,
1676    coverage: &[u8],
1677    bg: Rgb565,
1678    width: usize,
1679    height: usize,
1680) where
1681    D: DrawTarget<Color = Rgb565> + ReadPixel,
1682    <D as DrawTarget>::Error: Debug,
1683{
1684    for y in 0..height {
1685        for x in 0..width {
1686            let idx = y * width + x;
1687            let cov = coverage[idx];
1688            if cov == 255 {
1689                continue; // pixel fully owned by triangles, nothing to do
1690            }
1691            let p = Point::new(x as i32, y as i32);
1692            let final_color = if cov == 0 {
1693                bg
1694            } else {
1695                // Pixel holds the (already pre-composited) accumulated
1696                // triangle color, weighted by `cov / 255`. Composite the
1697                // remaining `(255 - cov) / 255` with the bg.
1698                let tri_color = fb.read_pixel(p);
1699                // Convert 0..255 to 0..256 q8 coverage for blend_q8.
1700                let cov_q8 = ((cov as u32) * 256) / 255;
1701                blend_q8(bg, tri_color, cov_q8)
1702            };
1703            fb.draw_iter([embedded_graphics_core::Pixel(p, final_color)])
1704                .unwrap();
1705        }
1706    }
1707}
1708
1709/// Z-test + coverage-tracked write of a single AA pixel.
1710///
1711/// Four cases, branched on `coverage_q8` (this triangle's pixel coverage)
1712/// and `coverage[idx]` (sum of prior triangles' coverage at this pixel):
1713///
1714/// 1. Full coverage (`coverage_q8 >= 256`): triangle covers the pixel
1715///    completely. Overwrite. Coverage saturates to 255.
1716/// 2. Virgin pixel + partial: store pure triangle color; bg gets composited
1717///    by `composite_aa_background` at end-of-frame using the claimed cov.
1718/// 3. Already-fully-covered pixel + partial closer triangle: blend the new
1719///    color over the existing (existing acts as the local "background"
1720///    since it's the visible scene behind the new triangle).
1721/// 4. Partially-claimed pixel + partial new: weighted-average accumulation
1722///    of pure triangle colors. This is the shared-coplanar-edge case.
1723#[cfg(feature = "aa-coverage")]
1724#[inline(always)]
1725fn aa_pixel_cov<D>(
1726    fb: &mut D,
1727    x: i32,
1728    y: i32,
1729    color: Rgb565,
1730    z: u32,
1731    zbuffer: &mut [crate::ZDepth],
1732    coverage: &mut [u8],
1733    width: usize,
1734    coverage_q8: u32,
1735) where
1736    D: DrawTarget<Color = Rgb565> + ReadPixel,
1737    <D as DrawTarget>::Error: Debug,
1738{
1739    if x < 0 || y < 0 || x >= width as i32 || coverage_q8 == 0 {
1740        return;
1741    }
1742    let idx = y as usize * width + x as usize;
1743    if idx >= zbuffer.len() {
1744        return;
1745    }
1746    let z_depth = crate::to_zdepth(z);
1747    if z_depth >= zbuffer[idx].saturating_add(crate::DEPTH_EPSILON) {
1748        return;
1749    }
1750
1751    let p = Point::new(x, y);
1752
1753    // Case 1: full coverage — overwrite unconditionally.
1754    if coverage_q8 >= 256 {
1755        coverage[idx] = 255;
1756        zbuffer[idx] = z_depth;
1757        fb.draw_iter([embedded_graphics_core::Pixel(p, color)])
1758            .unwrap();
1759        return;
1760    }
1761
1762    let prev_cov = coverage[idx] as u32;
1763
1764    if prev_cov == 0 {
1765        // Case 2: virgin pixel + partial coverage. Pure triangle color;
1766        // bg composite at end of frame fills the unclaimed remainder.
1767        let claim_255 = (coverage_q8 * 255) >> 8;
1768        coverage[idx] = claim_255 as u8;
1769        zbuffer[idx] = z_depth;
1770        fb.draw_iter([embedded_graphics_core::Pixel(p, color)])
1771            .unwrap();
1772        return;
1773    }
1774
1775    if prev_cov >= 255 {
1776        // Case 3: pixel was fully claimed by farther geometry. We're closer
1777        // (z-test passed). Anti-alias the new triangle's edge against the
1778        // existing pixel as if it were the local background. Total coverage
1779        // stays at 255 — no bg composite needed.
1780        let existing = fb.read_pixel(p);
1781        let result = blend_q8(existing, color, coverage_q8);
1782        zbuffer[idx] = z_depth;
1783        fb.draw_iter([embedded_graphics_core::Pixel(p, result)])
1784            .unwrap();
1785        return;
1786    }
1787
1788    // Case 4: partially-claimed pixel + partial new triangle. Weighted-
1789    // average accumulation. This is the coplanar-shared-edge case.
1790    let remaining = 255 - prev_cov;
1791    let claim_255 = ((coverage_q8 * 255) >> 8).min(remaining);
1792    if claim_255 == 0 {
1793        return;
1794    }
1795    let new_total = prev_cov + claim_255;
1796    let existing = fb.read_pixel(p);
1797    let blend_factor = (claim_255 * 256) / new_total;
1798    let result = blend_q8(existing, color, blend_factor);
1799    coverage[idx] = new_total as u8;
1800    zbuffer[idx] = z_depth;
1801    fb.draw_iter([embedded_graphics_core::Pixel(p, result)])
1802        .unwrap();
1803}
1804
1805#[cfg(feature = "aa-coverage")]
1806#[inline(always)]
1807fn fill_triangle_zbuffered_aa_cov<D>(
1808    p1: nalgebra::Point2<i32>,
1809    p2: nalgebra::Point2<i32>,
1810    p3: nalgebra::Point2<i32>,
1811    z1: f32,
1812    z2: f32,
1813    z3: f32,
1814    color: Rgb565,
1815    fb: &mut D,
1816    zbuffer: &mut [crate::ZDepth],
1817    coverage: &mut [u8],
1818    width: usize,
1819) where
1820    D: DrawTarget<Color = Rgb565> + ReadPixel,
1821    <D as DrawTarget>::Error: Debug,
1822{
1823    let p1_eg = Point::new(p1.x, p1.y);
1824    let p2_eg = Point::new(p2.x, p2.y);
1825    let p3_eg = Point::new(p3.x, p3.y);
1826
1827    let z1_int = (z1 * 65536.0) as u32;
1828    let z2_int = (z2 * 65536.0) as u32;
1829    let z3_int = (z3 * 65536.0) as u32;
1830
1831    if p2_eg.y == p3_eg.y {
1832        fill_bottom_flat_aa_cov(
1833            p1_eg, p2_eg, p3_eg, z1_int, z2_int, z3_int, color, fb, zbuffer, coverage, width,
1834        );
1835    } else if p1_eg.y == p2_eg.y {
1836        fill_top_flat_aa_cov(
1837            p1_eg, p2_eg, p3_eg, z1_int, z2_int, z3_int, color, fb, zbuffer, coverage, width,
1838        );
1839    } else {
1840        let t = (p2_eg.y - p1_eg.y) as f32 / (p3_eg.y - p1_eg.y) as f32;
1841        let p4 = Point::new(
1842            (p1_eg.x as f32 + t * (p3_eg.x - p1_eg.x) as f32) as i32,
1843            p2_eg.y,
1844        );
1845        let z4_int = (z1_int as i64 + (t * (z3_int as i64 - z1_int as i64) as f32) as i64) as u32;
1846        fill_bottom_flat_aa_cov(
1847            p1_eg, p2_eg, p4, z1_int, z2_int, z4_int, color, fb, zbuffer, coverage, width,
1848        );
1849        fill_top_flat_aa_cov(
1850            p2_eg, p4, p3_eg, z2_int, z4_int, z3_int, color, fb, zbuffer, coverage, width,
1851        );
1852    }
1853}
1854
1855#[cfg(feature = "aa-coverage")]
1856#[inline(always)]
1857fn fill_bottom_flat_aa_cov<D>(
1858    p1: Point,
1859    p2: Point,
1860    p3: Point,
1861    z1: u32,
1862    z2: u32,
1863    z3: u32,
1864    color: Rgb565,
1865    fb: &mut D,
1866    zbuffer: &mut [crate::ZDepth],
1867    coverage: &mut [u8],
1868    width: usize,
1869) where
1870    D: DrawTarget<Color = Rgb565> + ReadPixel,
1871    <D as DrawTarget>::Error: Debug,
1872{
1873    let height = p2.y - p1.y;
1874    if height == 0 {
1875        return;
1876    }
1877    let invslope1 = ((p2.x - p1.x) << 16) / height;
1878    let invslope2 = ((p3.x - p1.x) << 16) / height;
1879
1880    let mut curx1 = p1.x << 16;
1881    let mut curx2 = p1.x << 16;
1882
1883    for scanline_y in p1.y..=p2.y {
1884        let dy = scanline_y - p1.y;
1885        let z_left = (z1 as i64 + ((z2 as i64 - z1 as i64) * dy as i64 / height as i64)) as u32;
1886        let z_right = (z1 as i64 + ((z3 as i64 - z1 as i64) * dy as i64 / height as i64)) as u32;
1887
1888        aa_scanline_cov(
1889            curx1, curx2, scanline_y, z_left, z_right, color, fb, zbuffer, coverage, width,
1890        );
1891
1892        curx1 += invslope1;
1893        curx2 += invslope2;
1894    }
1895}
1896
1897#[cfg(feature = "aa-coverage")]
1898#[inline(always)]
1899fn fill_top_flat_aa_cov<D>(
1900    p1: Point,
1901    p2: Point,
1902    p3: Point,
1903    z1: u32,
1904    z2: u32,
1905    z3: u32,
1906    color: Rgb565,
1907    fb: &mut D,
1908    zbuffer: &mut [crate::ZDepth],
1909    coverage: &mut [u8],
1910    width: usize,
1911) where
1912    D: DrawTarget<Color = Rgb565> + ReadPixel,
1913    <D as DrawTarget>::Error: Debug,
1914{
1915    let height = p3.y - p1.y;
1916    if height == 0 {
1917        return;
1918    }
1919    let invslope1 = ((p3.x - p1.x) << 16) / height;
1920    let invslope2 = ((p3.x - p2.x) << 16) / height;
1921
1922    let mut curx1 = p3.x << 16;
1923    let mut curx2 = p3.x << 16;
1924
1925    for scanline_y in (p1.y..=p3.y).rev() {
1926        let dy = scanline_y - p1.y;
1927        let z_left = (z1 as i64 + ((z3 as i64 - z1 as i64) * dy as i64 / height as i64)) as u32;
1928        let z_right = (z2 as i64 + ((z3 as i64 - z2 as i64) * dy as i64 / height as i64)) as u32;
1929
1930        aa_scanline_cov(
1931            curx1, curx2, scanline_y, z_left, z_right, color, fb, zbuffer, coverage, width,
1932        );
1933
1934        curx1 -= invslope1;
1935        curx2 -= invslope2;
1936    }
1937}
1938
1939#[cfg(feature = "aa-coverage")]
1940#[inline(always)]
1941fn aa_scanline_cov<D>(
1942    cx1: i32,
1943    cx2: i32,
1944    y: i32,
1945    z_left: u32,
1946    z_right: u32,
1947    color: Rgb565,
1948    fb: &mut D,
1949    zbuffer: &mut [crate::ZDepth],
1950    coverage: &mut [u8],
1951    width: usize,
1952) where
1953    D: DrawTarget<Color = Rgb565> + ReadPixel,
1954    <D as DrawTarget>::Error: Debug,
1955{
1956    let (left_fx, right_fx, z_l, z_r) = if cx1 <= cx2 {
1957        (cx1, cx2, z_left, z_right)
1958    } else {
1959        (cx2, cx1, z_right, z_left)
1960    };
1961
1962    let l_int = left_fx >> 16;
1963    let r_int = right_fx >> 16;
1964    let l_frac_q16 = (left_fx & 0xFFFF) as u32;
1965    let r_frac_q16 = (right_fx & 0xFFFF) as u32;
1966    let span = r_int - l_int;
1967
1968    if l_int == r_int {
1969        let cov_q16 = r_frac_q16.saturating_sub(l_frac_q16);
1970        aa_pixel_cov(
1971            fb,
1972            l_int,
1973            y,
1974            color,
1975            z_l,
1976            zbuffer,
1977            coverage,
1978            width,
1979            cov_q16 >> 8,
1980        );
1981        return;
1982    }
1983
1984    let left_cov_q8 = 256 - (l_frac_q16 >> 8);
1985    aa_pixel_cov(
1986        fb,
1987        l_int,
1988        y,
1989        color,
1990        z_l,
1991        zbuffer,
1992        coverage,
1993        width,
1994        left_cov_q8,
1995    );
1996
1997    if span > 1 {
1998        for x in (l_int + 1)..r_int {
1999            // Inner pixels are full coverage (q8 = 256). Use the same code
2000            // path as boundary pixels so coverage tracks correctly.
2001            let t_num = (x - l_int) as i64;
2002            let t_den = span as i64;
2003            let z = (z_l as i64 + ((z_r as i64 - z_l as i64) * t_num / t_den)) as u32;
2004            aa_pixel_cov(fb, x, y, color, z, zbuffer, coverage, width, 256);
2005        }
2006    }
2007
2008    if r_frac_q16 > 0 {
2009        let right_cov_q8 = r_frac_q16 >> 8;
2010        aa_pixel_cov(
2011            fb,
2012            r_int,
2013            y,
2014            color,
2015            z_r,
2016            zbuffer,
2017            coverage,
2018            width,
2019            right_cov_q8,
2020        );
2021    }
2022}
2023
2024/// Wu's anti-aliased line algorithm.
2025///
2026/// Walks the major axis one integer step at a time; at each step writes two
2027/// pixels straddling the line with complementary fractional coverage.
2028#[cfg(feature = "aa")]
2029pub fn draw_line_aa<D>(x0: i32, y0: i32, x1: i32, y1: i32, color: Rgb565, fb: &mut D)
2030where
2031    D: DrawTarget<Color = Rgb565> + ReadPixel,
2032    <D as DrawTarget>::Error: Debug,
2033{
2034    let dx = (x1 - x0).abs();
2035    let dy = (y1 - y0).abs();
2036    let steep = dy > dx;
2037    let (x0, y0, x1, y1) = if steep {
2038        (y0, x0, y1, x1)
2039    } else {
2040        (x0, y0, x1, y1)
2041    };
2042    let (x0, y0, x1, y1) = if x0 > x1 {
2043        (x1, y1, x0, y0)
2044    } else {
2045        (x0, y0, x1, y1)
2046    };
2047    let dx = x1 - x0;
2048    let dy = y1 - y0;
2049    if dx == 0 {
2050        // Single pixel
2051        let (px, py) = if steep { (y0, x0) } else { (x0, y0) };
2052        plot_aa(fb, px, py, color, 256);
2053        return;
2054    }
2055    // 16.16 fixed-point gradient
2056    let gradient: i32 = ((dy as i64) << 16) as i32 / dx;
2057    // Start at exact (x0, y0); intery accumulates the y position in 16.16.
2058    let mut intery: i32 = y0 << 16;
2059    for x in x0..=x1 {
2060        let y_int = intery >> 16;
2061        let frac_q16 = (intery & 0xFFFF) as u32;
2062        let cov_top = 256 - (frac_q16 >> 8); // pixel at y_int
2063        let cov_bot = frac_q16 >> 8; //         pixel at y_int + 1
2064        if steep {
2065            plot_aa(fb, y_int, x, color, cov_top);
2066            plot_aa(fb, y_int + 1, x, color, cov_bot);
2067        } else {
2068            plot_aa(fb, x, y_int, color, cov_top);
2069            plot_aa(fb, x, y_int + 1, color, cov_bot);
2070        }
2071        intery += gradient;
2072    }
2073}
2074
2075#[cfg(feature = "aa")]
2076#[inline(always)]
2077fn plot_aa<D>(fb: &mut D, x: i32, y: i32, color: Rgb565, coverage_q8: u32)
2078where
2079    D: DrawTarget<Color = Rgb565> + ReadPixel,
2080    <D as DrawTarget>::Error: Debug,
2081{
2082    if coverage_q8 == 0 {
2083        return;
2084    }
2085    let final_color = if coverage_q8 >= 256 {
2086        color
2087    } else {
2088        let bg = fb.read_pixel(Point::new(x, y));
2089        blend_q8(bg, color, coverage_q8)
2090    };
2091    fb.draw_iter([embedded_graphics_core::Pixel(Point::new(x, y), final_color)])
2092        .unwrap();
2093}
2094
2095// Z-buffered drawing function with optional fog and dithering effects
2096#[inline]
2097pub fn draw_zbuffered_with_effects<
2098    D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>,
2099>(
2100    primitive: DrawPrimitive,
2101    fb: &mut D,
2102    zbuffer: &mut [crate::ZDepth],
2103    width: usize,
2104    fog_config: Option<&FogConfig>,
2105    dither_config: Option<&DitherConfig>,
2106) where
2107    <D as DrawTarget>::Error: Debug,
2108{
2109    match primitive {
2110        DrawPrimitive::ColoredTriangleWithDepth {
2111            mut points,
2112            mut depths,
2113            color,
2114        } => {
2115            // Sort vertices by y coordinate (and corresponding depths)
2116            if points[0].y > points[1].y {
2117                points.swap(0, 1);
2118                depths.swap(0, 1);
2119            }
2120            if points[0].y > points[2].y {
2121                points.swap(0, 2);
2122                depths.swap(0, 2);
2123            }
2124            if points[1].y > points[2].y {
2125                points.swap(1, 2);
2126                depths.swap(1, 2);
2127            }
2128
2129            let [p1, p2, p3] = points;
2130            let [z1, z2, z3] = depths;
2131
2132            // Off-screen culling.
2133            let scr_w = width as i32;
2134            let scr_h = (zbuffer.len() / width) as i32;
2135            if p1.x < 0 && p2.x < 0 && p3.x < 0 {
2136                return;
2137            }
2138            if p1.x >= scr_w && p2.x >= scr_w && p3.x >= scr_w {
2139                return;
2140            }
2141            if p1.y < 0 && p2.y < 0 && p3.y < 0 {
2142                return;
2143            }
2144            if p1.y >= scr_h && p2.y >= scr_h && p3.y >= scr_h {
2145                return;
2146            }
2147
2148            fill_triangle_zbuffered(
2149                p1,
2150                p2,
2151                p3,
2152                z1,
2153                z2,
2154                z3,
2155                color,
2156                fb,
2157                zbuffer,
2158                width,
2159                fog_config,
2160                dither_config,
2161            );
2162        }
2163        DrawPrimitive::TranslucentTriangleWithDepth {
2164            mut points,
2165            mut depths,
2166            color,
2167            alpha,
2168        } => {
2169            if points[0].y > points[1].y {
2170                points.swap(0, 1);
2171                depths.swap(0, 1);
2172            }
2173            if points[0].y > points[2].y {
2174                points.swap(0, 2);
2175                depths.swap(0, 2);
2176            }
2177            if points[1].y > points[2].y {
2178                points.swap(1, 2);
2179                depths.swap(1, 2);
2180            }
2181
2182            let [p1, p2, p3] = points;
2183            let [z1, z2, z3] = depths;
2184
2185            let scr_w = width as i32;
2186            let scr_h = (zbuffer.len() / width) as i32;
2187            if p1.x < 0 && p2.x < 0 && p3.x < 0 {
2188                return;
2189            }
2190            if p1.x >= scr_w && p2.x >= scr_w && p3.x >= scr_w {
2191                return;
2192            }
2193            if p1.y < 0 && p2.y < 0 && p3.y < 0 {
2194                return;
2195            }
2196            if p1.y >= scr_h && p2.y >= scr_h && p3.y >= scr_h {
2197                return;
2198            }
2199
2200            fill_triangle_zbuffered_translucent(
2201                p1, p2, p3, z1, z2, z3, color, alpha, fb, zbuffer, width,
2202            );
2203        }
2204        #[cfg(feature = "lighting")]
2205        DrawPrimitive::GouraudTriangleWithDepth {
2206            mut points,
2207            mut depths,
2208            mut colors,
2209        } => {
2210            // Sort vertices by y coordinate (and corresponding depths and colors)
2211            if points[0].y > points[1].y {
2212                points.swap(0, 1);
2213                depths.swap(0, 1);
2214                colors.swap(0, 1);
2215            }
2216            if points[0].y > points[2].y {
2217                points.swap(0, 2);
2218                depths.swap(0, 2);
2219                colors.swap(0, 2);
2220            }
2221            if points[1].y > points[2].y {
2222                points.swap(1, 2);
2223                depths.swap(1, 2);
2224                colors.swap(1, 2);
2225            }
2226
2227            let [p1, p2, p3] = points;
2228            let [z1, z2, z3] = depths;
2229            let [c1, c2, c3] = colors;
2230
2231            // Off-screen culling.
2232            let scr_w = width as i32;
2233            let scr_h = (zbuffer.len() / width) as i32;
2234            if p1.x < 0 && p2.x < 0 && p3.x < 0 {
2235                return;
2236            }
2237            if p1.x >= scr_w && p2.x >= scr_w && p3.x >= scr_w {
2238                return;
2239            }
2240            if p1.y < 0 && p2.y < 0 && p3.y < 0 {
2241                return;
2242            }
2243            if p1.y >= scr_h && p2.y >= scr_h && p3.y >= scr_h {
2244                return;
2245            }
2246
2247            fill_triangle_zbuffered_gouraud(
2248                p1,
2249                p2,
2250                p3,
2251                z1,
2252                z2,
2253                z3,
2254                c1,
2255                c2,
2256                c3,
2257                fb,
2258                zbuffer,
2259                width,
2260                fog_config,
2261                dither_config,
2262            );
2263        }
2264        // Textured / lightmapped triangles require a texture manager.
2265        #[cfg(feature = "textured")]
2266        DrawPrimitive::TexturedTriangle { .. }
2267        | DrawPrimitive::TexturedTriangleWithDepth { .. }
2268        | DrawPrimitive::TexturedGouraudTriangleWithDepth { .. }
2269        | DrawPrimitive::LightmappedTriangle { .. } => {
2270            // Use draw_zbuffered_with_textures() / draw_zbuffered_lightmapped() instead.
2271        }
2272        // For other primitives, fall back to regular drawing
2273        _ => draw(primitive, fb),
2274    }
2275}
2276
2277#[cfg(feature = "textured")]
2278#[inline(always)]
2279fn interpolate_uv(
2280    t: f32,
2281    w1: f32,
2282    w2: f32,
2283    uv1: [f32; 2],
2284    uv2: [f32; 2],
2285    texture_mapping: TextureMapping,
2286) -> [f32; 2] {
2287    match texture_mapping {
2288        TextureMapping::PerspectiveCorrect => {
2289            let ow1 = 1.0 / w1;
2290            let ow2 = 1.0 / w2;
2291            let one_over_w = ow1 + t * (ow2 - ow1);
2292            [
2293                (uv1[0] * ow1 + t * (uv2[0] * ow2 - uv1[0] * ow1)) / one_over_w,
2294                (uv1[1] * ow1 + t * (uv2[1] * ow2 - uv1[1] * ow1)) / one_over_w,
2295            ]
2296        }
2297        TextureMapping::Affine => [
2298            uv1[0] + t * (uv2[0] - uv1[0]),
2299            uv1[1] + t * (uv2[1] - uv1[1]),
2300        ],
2301    }
2302}
2303
2304#[cfg(feature = "textured")]
2305#[inline(always)]
2306fn should_skip_stipple(x: i32, y: i32, stipple_mode: StippleMode) -> bool {
2307    matches!(stipple_mode, StippleMode::Checkerboard) && ((x ^ y) & 1) != 0
2308}
2309
2310#[cfg(feature = "textured")]
2311// Z-buffered drawing function with textures, fog, and dithering effects
2312#[inline]
2313pub fn draw_zbuffered_with_textures<
2314    D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>,
2315    const N: usize,
2316>(
2317    primitive: DrawPrimitive,
2318    fb: &mut D,
2319    zbuffer: &mut [crate::ZDepth],
2320    width: usize,
2321    texture_manager: &crate::texture::TextureManager<N>,
2322    fog_config: Option<&FogConfig>,
2323    dither_config: Option<&DitherConfig>,
2324) where
2325    <D as DrawTarget>::Error: Debug,
2326{
2327    draw_zbuffered_with_textures_mapped(
2328        primitive,
2329        fb,
2330        zbuffer,
2331        width,
2332        texture_manager,
2333        fog_config,
2334        dither_config,
2335        TextureMapping::PerspectiveCorrect,
2336        StippleMode::Off,
2337        None,
2338        PaletteMode::Off,
2339    );
2340}
2341
2342#[cfg(feature = "textured")]
2343#[inline]
2344pub fn draw_zbuffered_with_textures_mapped<
2345    D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>,
2346    const N: usize,
2347>(
2348    primitive: DrawPrimitive,
2349    fb: &mut D,
2350    zbuffer: &mut [crate::ZDepth],
2351    width: usize,
2352    texture_manager: &crate::texture::TextureManager<N>,
2353    fog_config: Option<&FogConfig>,
2354    dither_config: Option<&DitherConfig>,
2355    texture_mapping: TextureMapping,
2356    stipple_mode: StippleMode,
2357    screen_tint: Option<ScreenTint>,
2358    palette_mode: PaletteMode,
2359) where
2360    <D as DrawTarget>::Error: Debug,
2361{
2362    match primitive {
2363        #[cfg(feature = "textured")]
2364        DrawPrimitive::TexturedTriangleWithDepth {
2365            mut points,
2366            mut depths,
2367            mut ws,
2368            mut uvs,
2369            texture_id,
2370        } => {
2371            // Get texture from manager
2372            if let Some(texture) = texture_manager.get(texture_id) {
2373                // Sort vertices by y coordinate (and corresponding depths, ws, and UVs)
2374                if points[0].y > points[1].y {
2375                    points.swap(0, 1);
2376                    depths.swap(0, 1);
2377                    ws.swap(0, 1);
2378                    uvs.swap(0, 1);
2379                }
2380                if points[0].y > points[2].y {
2381                    points.swap(0, 2);
2382                    depths.swap(0, 2);
2383                    ws.swap(0, 2);
2384                    uvs.swap(0, 2);
2385                }
2386                if points[1].y > points[2].y {
2387                    points.swap(1, 2);
2388                    depths.swap(1, 2);
2389                    ws.swap(1, 2);
2390                    uvs.swap(1, 2);
2391                }
2392
2393                let [p1, p2, p3] = points;
2394                let [z1, z2, z3] = depths;
2395                let [w1, w2, w3] = ws;
2396                let [uv1, uv2, uv3] = uvs;
2397
2398                // Off-screen culling.
2399                let scr_w = width as i32;
2400                let scr_h = (zbuffer.len() / width) as i32;
2401                if p1.x < 0 && p2.x < 0 && p3.x < 0 {
2402                    return;
2403                }
2404                if p1.x >= scr_w && p2.x >= scr_w && p3.x >= scr_w {
2405                    return;
2406                }
2407                if p1.y < 0 && p2.y < 0 && p3.y < 0 {
2408                    return;
2409                }
2410                if p1.y >= scr_h && p2.y >= scr_h && p3.y >= scr_h {
2411                    return;
2412                }
2413
2414                fill_triangle_zbuffered_textured(
2415                    p1,
2416                    p2,
2417                    p3,
2418                    z1,
2419                    z2,
2420                    z3,
2421                    w1,
2422                    w2,
2423                    w3,
2424                    uv1,
2425                    uv2,
2426                    uv3,
2427                    texture,
2428                    fb,
2429                    zbuffer,
2430                    width,
2431                    fog_config,
2432                    dither_config,
2433                    texture_mapping,
2434                    stipple_mode,
2435                    screen_tint,
2436                    palette_mode,
2437                );
2438            }
2439        }
2440        #[cfg(feature = "textured")]
2441        DrawPrimitive::TexturedGouraudTriangleWithDepth {
2442            mut points,
2443            mut depths,
2444            mut ws,
2445            mut uvs,
2446            mut colors,
2447            texture_id,
2448        } => {
2449            if let Some(texture) = texture_manager.get(texture_id) {
2450                if points[0].y > points[1].y {
2451                    points.swap(0, 1);
2452                    depths.swap(0, 1);
2453                    ws.swap(0, 1);
2454                    uvs.swap(0, 1);
2455                    colors.swap(0, 1);
2456                }
2457                if points[0].y > points[2].y {
2458                    points.swap(0, 2);
2459                    depths.swap(0, 2);
2460                    ws.swap(0, 2);
2461                    uvs.swap(0, 2);
2462                    colors.swap(0, 2);
2463                }
2464                if points[1].y > points[2].y {
2465                    points.swap(1, 2);
2466                    depths.swap(1, 2);
2467                    ws.swap(1, 2);
2468                    uvs.swap(1, 2);
2469                    colors.swap(1, 2);
2470                }
2471
2472                let [p1, p2, p3] = points;
2473                let [z1, z2, z3] = depths;
2474                let [w1, w2, w3] = ws;
2475                let [uv1, uv2, uv3] = uvs;
2476                let [c1, c2, c3] = colors;
2477
2478                let scr_w = width as i32;
2479                let scr_h = (zbuffer.len() / width) as i32;
2480                if p1.x < 0 && p2.x < 0 && p3.x < 0 {
2481                    return;
2482                }
2483                if p1.x >= scr_w && p2.x >= scr_w && p3.x >= scr_w {
2484                    return;
2485                }
2486                if p1.y < 0 && p2.y < 0 && p3.y < 0 {
2487                    return;
2488                }
2489                if p1.y >= scr_h && p2.y >= scr_h && p3.y >= scr_h {
2490                    return;
2491                }
2492
2493                fill_triangle_zbuffered_textured_gouraud(
2494                    p1,
2495                    p2,
2496                    p3,
2497                    z1,
2498                    z2,
2499                    z3,
2500                    w1,
2501                    w2,
2502                    w3,
2503                    uv1,
2504                    uv2,
2505                    uv3,
2506                    c1,
2507                    c2,
2508                    c3,
2509                    texture,
2510                    fb,
2511                    zbuffer,
2512                    width,
2513                    fog_config,
2514                    dither_config,
2515                    texture_mapping,
2516                    stipple_mode,
2517                    screen_tint,
2518                    palette_mode,
2519                );
2520            }
2521        }
2522        // For other primitives, fall back to regular z-buffered drawing
2523        _ => draw_zbuffered_with_effects(primitive, fb, zbuffer, width, fog_config, dither_config),
2524    }
2525}
2526
2527#[cfg(feature = "textured")]
2528// ---------------------------------------------------------------------------
2529// Lightmapped triangle (M6)
2530// ---------------------------------------------------------------------------
2531
2532/// Rasterise a perspective-correct textured triangle multiplied by a lightmap.
2533///
2534/// Both the surface texture and the lightmap are looked up via `texture_manager`.
2535/// The final colour per pixel is a per-channel normalised product:
2536/// `lit_r = (surf.r * lm.r) / 31`, etc.
2537///
2538/// If either texture ID is missing the triangle is skipped silently.
2539/// Passing `lightmap_id = u32::MAX` renders the surface texture at full
2540/// brightness (no lightmap multiply).
2541pub fn draw_zbuffered_lightmapped<
2542    D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>,
2543    const N: usize,
2544>(
2545    points: [nalgebra::Point2<i32>; 3],
2546    depths: [f32; 3],
2547    ws: [f32; 3],
2548    surface_uvs: [[f32; 2]; 3],
2549    lm_uvs: [[f32; 2]; 3],
2550    texture_id: u32,
2551    lightmap_id: u32,
2552    brightness: u8,
2553    dynamic_tint: embedded_graphics_core::pixelcolor::Rgb565,
2554    fog_config: Option<&FogConfig>,
2555    texture_manager: &crate::texture::TextureManager<N>,
2556    fb: &mut D,
2557    zbuffer: &mut [crate::ZDepth],
2558    width: usize,
2559) where
2560    <D as DrawTarget>::Error: core::fmt::Debug,
2561{
2562    draw_zbuffered_lightmapped_mapped(
2563        points,
2564        depths,
2565        ws,
2566        surface_uvs,
2567        lm_uvs,
2568        texture_id,
2569        lightmap_id,
2570        brightness,
2571        dynamic_tint,
2572        fog_config,
2573        texture_manager,
2574        fb,
2575        zbuffer,
2576        width,
2577        TextureMapping::PerspectiveCorrect,
2578        StippleMode::Off,
2579        None,
2580        PaletteMode::Off,
2581    );
2582}
2583
2584#[cfg(feature = "textured")]
2585pub fn draw_zbuffered_lightmapped_mapped<
2586    D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>,
2587    const N: usize,
2588>(
2589    mut points: [nalgebra::Point2<i32>; 3],
2590    mut depths: [f32; 3],
2591    mut ws: [f32; 3],
2592    mut surface_uvs: [[f32; 2]; 3],
2593    mut lm_uvs: [[f32; 2]; 3],
2594    texture_id: u32,
2595    lightmap_id: u32,
2596    brightness: u8,
2597    dynamic_tint: embedded_graphics_core::pixelcolor::Rgb565,
2598    fog_config: Option<&FogConfig>,
2599    texture_manager: &crate::texture::TextureManager<N>,
2600    fb: &mut D,
2601    zbuffer: &mut [crate::ZDepth],
2602    width: usize,
2603    texture_mapping: TextureMapping,
2604    stipple_mode: StippleMode,
2605    screen_tint: Option<ScreenTint>,
2606    palette_mode: PaletteMode,
2607) where
2608    <D as DrawTarget>::Error: core::fmt::Debug,
2609{
2610    let surf = match texture_manager.get(texture_id) {
2611        Some(t) => t,
2612        None => return,
2613    };
2614    let lm = if lightmap_id == u32::MAX {
2615        None
2616    } else {
2617        texture_manager.get(lightmap_id)
2618    };
2619
2620    // Sort vertices by Y (top to bottom)
2621    macro_rules! swap_all {
2622        ($i:expr, $j:expr) => {
2623            points.swap($i, $j);
2624            depths.swap($i, $j);
2625            ws.swap($i, $j);
2626            surface_uvs.swap($i, $j);
2627            lm_uvs.swap($i, $j);
2628        };
2629    }
2630    if points[0].y > points[1].y {
2631        swap_all!(0, 1);
2632    }
2633    if points[0].y > points[2].y {
2634        swap_all!(0, 2);
2635    }
2636    if points[1].y > points[2].y {
2637        swap_all!(1, 2);
2638    }
2639
2640    let [p1, p2, p3] = points;
2641    let [z1, z2, z3] = depths;
2642    let [w1, w2, w3] = ws;
2643    let [uv1, uv2, uv3] = surface_uvs;
2644    let [luv1, luv2, luv3] = lm_uvs;
2645
2646    let scr_w = width as i32;
2647    let scr_h = (zbuffer.len() / width) as i32;
2648    if p1.x < 0 && p2.x < 0 && p3.x < 0 {
2649        return;
2650    }
2651    if p1.x >= scr_w && p2.x >= scr_w && p3.x >= scr_w {
2652        return;
2653    }
2654    if p1.y < 0 && p2.y < 0 && p3.y < 0 {
2655        return;
2656    }
2657    if p1.y >= scr_h && p2.y >= scr_h && p3.y >= scr_h {
2658        return;
2659    }
2660
2661    let z1_int = (z1 * 65536.0) as u32;
2662    let z2_int = (z2 * 65536.0) as u32;
2663    let z3_int = (z3 * 65536.0) as u32;
2664
2665    // Split into flat-bottom + flat-top halves (same as existing textured path)
2666    if p2.y == p3.y {
2667        fill_lm_bottom_flat(
2668            p1,
2669            p2,
2670            p3,
2671            z1_int,
2672            z2_int,
2673            z3_int,
2674            w1,
2675            w2,
2676            w3,
2677            uv1,
2678            uv2,
2679            uv3,
2680            luv1,
2681            luv2,
2682            luv3,
2683            dynamic_tint,
2684            fog_config,
2685            surf,
2686            lm,
2687            fb,
2688            zbuffer,
2689            width,
2690            texture_mapping,
2691            stipple_mode,
2692            screen_tint,
2693            palette_mode,
2694            brightness,
2695        );
2696    } else if p1.y == p2.y {
2697        fill_lm_top_flat(
2698            p1,
2699            p2,
2700            p3,
2701            z1_int,
2702            z2_int,
2703            z3_int,
2704            w1,
2705            w2,
2706            w3,
2707            uv1,
2708            uv2,
2709            uv3,
2710            luv1,
2711            luv2,
2712            luv3,
2713            dynamic_tint,
2714            fog_config,
2715            surf,
2716            lm,
2717            fb,
2718            zbuffer,
2719            width,
2720            texture_mapping,
2721            stipple_mode,
2722            screen_tint,
2723            palette_mode,
2724            brightness,
2725        );
2726    } else {
2727        // Split at the middle vertex
2728        let dy31 = (p3.y - p1.y) as f32;
2729        let dy21 = (p2.y - p1.y) as f32;
2730        let t = dy21 / dy31;
2731        let p4x = p1.x + ((p3.x - p1.x) as f32 * t) as i32;
2732        let p4 = embedded_graphics_core::prelude::Point::new(p4x, p2.y);
2733        let z4_int = (z1_int as f32 + (z3_int as f32 - z1_int as f32) * t) as u32;
2734        let w4 = w1 + (w3 - w1) * t;
2735        let uv4 = [
2736            uv1[0] + (uv3[0] - uv1[0]) * t,
2737            uv1[1] + (uv3[1] - uv1[1]) * t,
2738        ];
2739        let luv4 = [
2740            luv1[0] + (luv3[0] - luv1[0]) * t,
2741            luv1[1] + (luv3[1] - luv1[1]) * t,
2742        ];
2743        let p4_2 = nalgebra::Point2::new(p4.x, p4.y);
2744        fill_lm_bottom_flat(
2745            p1,
2746            p2,
2747            p4_2,
2748            z1_int,
2749            z2_int,
2750            z4_int,
2751            w1,
2752            w2,
2753            w4,
2754            uv1,
2755            uv2,
2756            uv4,
2757            luv1,
2758            luv2,
2759            luv4,
2760            dynamic_tint,
2761            fog_config,
2762            surf,
2763            lm,
2764            fb,
2765            zbuffer,
2766            width,
2767            texture_mapping,
2768            stipple_mode,
2769            screen_tint,
2770            palette_mode,
2771            brightness,
2772        );
2773        fill_lm_top_flat(
2774            p2,
2775            p4_2,
2776            p3,
2777            z2_int,
2778            z4_int,
2779            z3_int,
2780            w2,
2781            w4,
2782            w3,
2783            uv2,
2784            uv4,
2785            uv3,
2786            luv2,
2787            luv4,
2788            luv3,
2789            dynamic_tint,
2790            fog_config,
2791            surf,
2792            lm,
2793            fb,
2794            zbuffer,
2795            width,
2796            texture_mapping,
2797            stipple_mode,
2798            screen_tint,
2799            palette_mode,
2800            brightness,
2801        );
2802    }
2803}
2804
2805#[cfg(feature = "textured")]
2806#[inline(always)]
2807#[allow(clippy::too_many_arguments)]
2808fn fill_lm_bottom_flat<D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>>(
2809    p1: nalgebra::Point2<i32>,
2810    p2: nalgebra::Point2<i32>,
2811    p3: nalgebra::Point2<i32>,
2812    z1: u32,
2813    z2: u32,
2814    z3: u32,
2815    w1: f32,
2816    w2: f32,
2817    w3: f32,
2818    uv1: [f32; 2],
2819    uv2: [f32; 2],
2820    uv3: [f32; 2],
2821    luv1: [f32; 2],
2822    luv2: [f32; 2],
2823    luv3: [f32; 2],
2824    dynamic_tint: embedded_graphics_core::pixelcolor::Rgb565,
2825    fog_config: Option<&FogConfig>,
2826    surf: &crate::texture::Texture,
2827    lm: Option<&crate::texture::Texture>,
2828    fb: &mut D,
2829    zbuffer: &mut [crate::ZDepth],
2830    width: usize,
2831    texture_mapping: TextureMapping,
2832    stipple_mode: StippleMode,
2833    screen_tint: Option<ScreenTint>,
2834    palette_mode: PaletteMode,
2835    brightness: u8,
2836) where
2837    <D as DrawTarget>::Error: core::fmt::Debug,
2838{
2839    let height = p2.y - p1.y;
2840    if height == 0 {
2841        return;
2842    }
2843    let invslope1 = ((p2.x - p1.x) << 16) / height;
2844    let invslope2 = ((p3.x - p1.x) << 16) / height;
2845    let mut curx1 = p1.x << 16;
2846    let mut curx2 = p1.x << 16;
2847    for scanline_y in p1.y..=p2.y {
2848        let dy = scanline_y - p1.y;
2849        let t = dy as f32 / height as f32;
2850        let z_l = (z1 as i64 + (z2 as i64 - z1 as i64) * dy as i64 / height as i64) as u32;
2851        let z_r = (z1 as i64 + (z3 as i64 - z1 as i64) * dy as i64 / height as i64) as u32;
2852        let wl = w1 + t * (w2 - w1);
2853        let wr = w1 + t * (w3 - w1);
2854        let uvl = [
2855            uv1[0] + t * (uv2[0] - uv1[0]),
2856            uv1[1] + t * (uv2[1] - uv1[1]),
2857        ];
2858        let uvr = [
2859            uv1[0] + t * (uv3[0] - uv1[0]),
2860            uv1[1] + t * (uv3[1] - uv1[1]),
2861        ];
2862        let luvl = [
2863            luv1[0] + t * (luv2[0] - luv1[0]),
2864            luv1[1] + t * (luv2[1] - luv1[1]),
2865        ];
2866        let luvr = [
2867            luv1[0] + t * (luv3[0] - luv1[0]),
2868            luv1[1] + t * (luv3[1] - luv1[1]),
2869        ];
2870        draw_scanline_lm(
2871            curx1 >> 16,
2872            curx2 >> 16,
2873            scanline_y,
2874            z_l,
2875            z_r,
2876            wl,
2877            wr,
2878            uvl,
2879            uvr,
2880            luvl,
2881            luvr,
2882            dynamic_tint,
2883            fog_config,
2884            surf,
2885            lm,
2886            fb,
2887            zbuffer,
2888            width,
2889            texture_mapping,
2890            stipple_mode,
2891            screen_tint,
2892            palette_mode,
2893            brightness,
2894        );
2895        curx1 += invslope1;
2896        curx2 += invslope2;
2897    }
2898}
2899
2900#[cfg(feature = "textured")]
2901#[inline(always)]
2902#[allow(clippy::too_many_arguments)]
2903fn fill_lm_top_flat<D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>>(
2904    p1: nalgebra::Point2<i32>,
2905    p2: nalgebra::Point2<i32>,
2906    p3: nalgebra::Point2<i32>,
2907    z1: u32,
2908    z2: u32,
2909    z3: u32,
2910    w1: f32,
2911    w2: f32,
2912    w3: f32,
2913    uv1: [f32; 2],
2914    uv2: [f32; 2],
2915    uv3: [f32; 2],
2916    luv1: [f32; 2],
2917    luv2: [f32; 2],
2918    luv3: [f32; 2],
2919    dynamic_tint: embedded_graphics_core::pixelcolor::Rgb565,
2920    fog_config: Option<&FogConfig>,
2921    surf: &crate::texture::Texture,
2922    lm: Option<&crate::texture::Texture>,
2923    fb: &mut D,
2924    zbuffer: &mut [crate::ZDepth],
2925    width: usize,
2926    texture_mapping: TextureMapping,
2927    stipple_mode: StippleMode,
2928    screen_tint: Option<ScreenTint>,
2929    palette_mode: PaletteMode,
2930    brightness: u8,
2931) where
2932    <D as DrawTarget>::Error: core::fmt::Debug,
2933{
2934    let height = p3.y - p1.y;
2935    if height == 0 {
2936        return;
2937    }
2938    let invslope1 = ((p3.x - p1.x) << 16) / height;
2939    let invslope2 = ((p3.x - p2.x) << 16) / height;
2940    let mut curx1 = p3.x << 16;
2941    let mut curx2 = p3.x << 16;
2942    for scanline_y in (p1.y..=p3.y).rev() {
2943        let dy = scanline_y - p1.y;
2944        let t = dy as f32 / height as f32;
2945        let z_l = (z1 as i64 + (z3 as i64 - z1 as i64) * dy as i64 / height as i64) as u32;
2946        let z_r = (z2 as i64 + (z3 as i64 - z2 as i64) * dy as i64 / height as i64) as u32;
2947        let wl = w1 + t * (w3 - w1);
2948        let wr = w2 + t * (w3 - w2);
2949        let uvl = [
2950            uv1[0] + t * (uv3[0] - uv1[0]),
2951            uv1[1] + t * (uv3[1] - uv1[1]),
2952        ];
2953        let uvr = [
2954            uv2[0] + t * (uv3[0] - uv2[0]),
2955            uv2[1] + t * (uv3[1] - uv2[1]),
2956        ];
2957        let luvl = [
2958            luv1[0] + t * (luv3[0] - luv1[0]),
2959            luv1[1] + t * (luv3[1] - luv1[1]),
2960        ];
2961        let luvr = [
2962            luv2[0] + t * (luv3[0] - luv2[0]),
2963            luv2[1] + t * (luv3[1] - luv2[1]),
2964        ];
2965        draw_scanline_lm(
2966            curx1 >> 16,
2967            curx2 >> 16,
2968            scanline_y,
2969            z_l,
2970            z_r,
2971            wl,
2972            wr,
2973            uvl,
2974            uvr,
2975            luvl,
2976            luvr,
2977            dynamic_tint,
2978            fog_config,
2979            surf,
2980            lm,
2981            fb,
2982            zbuffer,
2983            width,
2984            texture_mapping,
2985            stipple_mode,
2986            screen_tint,
2987            palette_mode,
2988            brightness,
2989        );
2990        curx1 -= invslope1;
2991        curx2 -= invslope2;
2992    }
2993}
2994
2995#[cfg(feature = "textured")]
2996#[inline(always)]
2997#[allow(clippy::too_many_arguments)]
2998fn draw_scanline_lm<D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>>(
2999    x1: i32,
3000    x2: i32,
3001    y: i32,
3002    z1: u32,
3003    z2: u32,
3004    w1: f32,
3005    w2: f32,
3006    uv1: [f32; 2],
3007    uv2: [f32; 2],
3008    luv1: [f32; 2],
3009    luv2: [f32; 2],
3010    dynamic_tint: embedded_graphics_core::pixelcolor::Rgb565,
3011    fog_config: Option<&FogConfig>,
3012    surf: &crate::texture::Texture,
3013    lm: Option<&crate::texture::Texture>,
3014    fb: &mut D,
3015    zbuffer: &mut [crate::ZDepth],
3016    width: usize,
3017    texture_mapping: TextureMapping,
3018    stipple_mode: StippleMode,
3019    screen_tint: Option<ScreenTint>,
3020    palette_mode: PaletteMode,
3021    brightness: u8,
3022) where
3023    <D as DrawTarget>::Error: core::fmt::Debug,
3024{
3025    use embedded_graphics_core::pixelcolor::RgbColor;
3026    use embedded_graphics_core::prelude::Point;
3027
3028    if y < 0 {
3029        return;
3030    }
3031    let height = zbuffer.len() / width;
3032    if y as usize >= height {
3033        return;
3034    }
3035
3036    let (left_x, right_x, z_left, z_right, w_left, w_right, uv_left, uv_right, luv_left, luv_right) =
3037        if x1 <= x2 {
3038            (x1, x2, z1, z2, w1, w2, uv1, uv2, luv1, luv2)
3039        } else {
3040            (x2, x1, z2, z1, w2, w1, uv2, uv1, luv2, luv1)
3041        };
3042
3043    let start_x = left_x.max(0);
3044    let end_x = right_x.min(width as i32 - 1);
3045    if start_x > end_x {
3046        return;
3047    }
3048
3049    let span = right_x - left_x;
3050    let inv_span = if span > 0 { 1.0 / span as f32 } else { 0.0 };
3051    let z_step = if span > 0 {
3052        (((z_right as i64 - z_left as i64) << 16) / span as i64) as i32
3053    } else {
3054        0
3055    };
3056
3057    let left_clip = start_x - left_x;
3058    let mut z_curr = ((z_left as i64) << 16) + (left_clip as i64 * z_step as i64);
3059    let mut zbuf_idx = y as usize * width + start_x as usize;
3060
3061    for x in start_x..=end_x {
3062        if should_skip_stipple(x, y, stipple_mode) {
3063            z_curr += z_step as i64;
3064            zbuf_idx += 1;
3065            continue;
3066        }
3067
3068        let z = (z_curr >> 16) as u32;
3069        z_curr += z_step as i64;
3070        let z_depth = crate::to_zdepth(z);
3071
3072        if z_depth >= zbuffer[zbuf_idx].saturating_add(crate::DEPTH_EPSILON) {
3073            zbuf_idx += 1;
3074            continue;
3075        }
3076        zbuffer[zbuf_idx] = z_depth;
3077
3078        let t = (x - left_x) as f32 * inv_span;
3079        let [su, sv] = interpolate_uv(t, w_left, w_right, uv_left, uv_right, texture_mapping);
3080        let surf_c = surf.sample(su, sv);
3081
3082        let lit_c = if let Some(lm_tex) = lm {
3083            let [lu, lv] = interpolate_uv(t, w_left, w_right, luv_left, luv_right, texture_mapping);
3084            let lm_c = lm_tex.sample(lu, lv);
3085            let r = ((surf_c.r() as u32 * lm_c.r() as u32) / 31).min(31) as u8;
3086            let g = ((surf_c.g() as u32 * lm_c.g() as u32) / 63).min(63) as u8;
3087            let b = ((surf_c.b() as u32 * lm_c.b() as u32) / 31).min(31) as u8;
3088            embedded_graphics_core::pixelcolor::Rgb565::new(r, g, b)
3089        } else {
3090            surf_c
3091        };
3092
3093        let lit_c = if brightness < 255 {
3094            let scale = brightness as u32;
3095            let r = ((lit_c.r() as u32 * scale) / 255) as u8;
3096            let g = ((lit_c.g() as u32 * scale) / 255) as u8;
3097            let b = ((lit_c.b() as u32 * scale) / 255) as u8;
3098            embedded_graphics_core::pixelcolor::Rgb565::new(r, g, b)
3099        } else {
3100            lit_c
3101        };
3102
3103        let tinted_c = embedded_graphics_core::pixelcolor::Rgb565::new(
3104            (lit_c.r() as u16 + dynamic_tint.r() as u16).min(31) as u8,
3105            (lit_c.g() as u16 + dynamic_tint.g() as u16).min(63) as u8,
3106            (lit_c.b() as u16 + dynamic_tint.b() as u16).min(31) as u8,
3107        );
3108
3109        let mut final_c = if let Some(fog) = fog_config {
3110            fog.apply(tinted_c, z)
3111        } else {
3112            tinted_c
3113        };
3114
3115        if let Some(tint) = screen_tint {
3116            final_c = tint.apply(final_c);
3117        }
3118        final_c = palette_mode.apply(final_c);
3119
3120        fb.draw_iter([embedded_graphics_core::Pixel(Point::new(x, y), final_c)])
3121            .unwrap();
3122    }
3123}
3124
3125#[cfg(all(feature = "textured", feature = "raycast"))]
3126// ---------------------------------------------------------------------------
3127// Coverage-based BSP rasteriser (M5 — no z-buffer)
3128// ---------------------------------------------------------------------------
3129
3130/// Rasterise a textured triangle using a coverage bitmap instead of a z-buffer.
3131///
3132/// Only writes pixels that have not yet been covered this frame.  Correct
3133/// when triangles arrive in strict front-to-back order (guaranteed by the BSP
3134/// walk in [`walk_front_to_back`](crate::bsp::traverse::walk_front_to_back)).
3135pub fn draw_bsp_coverage<
3136    D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>,
3137    const N: usize,
3138>(
3139    mut points: [nalgebra::Point2<i32>; 3],
3140    mut ws: [f32; 3],
3141    mut uvs: [[f32; 2]; 3],
3142    texture_id: u32,
3143    texture_manager: &crate::texture::TextureManager<N>,
3144    fb: &mut D,
3145    coverage: &mut crate::bsp::coverage::CoverageBuffer<'_>,
3146    texture_mapping: TextureMapping,
3147    stipple_mode: StippleMode,
3148    screen_tint: Option<ScreenTint>,
3149    palette_mode: PaletteMode,
3150) where
3151    <D as DrawTarget>::Error: core::fmt::Debug,
3152{
3153    let tex = match texture_manager.get(texture_id) {
3154        Some(t) => t,
3155        None => return,
3156    };
3157
3158    // Sort by Y
3159    if points[0].y > points[1].y {
3160        points.swap(0, 1);
3161        ws.swap(0, 1);
3162        uvs.swap(0, 1);
3163    }
3164    if points[0].y > points[2].y {
3165        points.swap(0, 2);
3166        ws.swap(0, 2);
3167        uvs.swap(0, 2);
3168    }
3169    if points[1].y > points[2].y {
3170        points.swap(1, 2);
3171        ws.swap(1, 2);
3172        uvs.swap(1, 2);
3173    }
3174
3175    let [p1, p2, p3] = points;
3176    let [w1, w2, w3] = ws;
3177    let [uv1, uv2, uv3] = uvs;
3178
3179    let w = coverage.width as i32;
3180    let h = coverage.height as i32;
3181    if p1.x < 0 && p2.x < 0 && p3.x < 0 {
3182        return;
3183    }
3184    if p1.x >= w && p2.x >= w && p3.x >= w {
3185        return;
3186    }
3187    if p1.y < 0 && p2.y < 0 && p3.y < 0 {
3188        return;
3189    }
3190    if p1.y >= h && p2.y >= h && p3.y >= h {
3191        return;
3192    }
3193
3194    let rasterize_span =
3195        |x1: i32,
3196         x2: i32,
3197         y: i32,
3198         wl: f32,
3199         wr: f32,
3200         uvl: [f32; 2],
3201         uvr: [f32; 2],
3202         fb: &mut D,
3203         coverage: &mut crate::bsp::coverage::CoverageBuffer<'_>| {
3204            let start = x1.min(x2);
3205            let end = x1.max(x2);
3206            let span = end - start;
3207            for x in start..=end {
3208                if x < 0 || y < 0 || x >= w || y >= h {
3209                    continue;
3210                }
3211                if coverage.is_covered(x as usize, y as usize) {
3212                    continue;
3213                }
3214                if should_skip_stipple(x, y, stipple_mode) {
3215                    continue;
3216                }
3217                let t = if span > 0 {
3218                    (x - start) as f32 / span as f32
3219                } else {
3220                    0.0
3221                };
3222                let [su, sv] = interpolate_uv(t, wl, wr, uvl, uvr, texture_mapping);
3223                let mut color = tex.sample(su, sv);
3224                if let Some(tint) = screen_tint {
3225                    color = tint.apply(color);
3226                }
3227                color = palette_mode.apply(color);
3228                coverage.mark_covered(x as usize, y as usize);
3229                fb.draw_iter([embedded_graphics_core::Pixel(
3230                    embedded_graphics_core::prelude::Point::new(x, y),
3231                    color,
3232                )])
3233                .unwrap();
3234            }
3235        };
3236
3237    // Flat-bottom triangle
3238    let draw_flat_bottom =
3239        |p1: nalgebra::Point2<i32>,
3240         p2: nalgebra::Point2<i32>,
3241         p3: nalgebra::Point2<i32>,
3242         w1: f32,
3243         w2: f32,
3244         w3: f32,
3245         uv1: [f32; 2],
3246         uv2: [f32; 2],
3247         uv3: [f32; 2],
3248         fb: &mut D,
3249         coverage: &mut crate::bsp::coverage::CoverageBuffer<'_>| {
3250            let height = p2.y - p1.y;
3251            if height == 0 {
3252                return;
3253            }
3254            let invslope1 = ((p2.x - p1.x) << 16) / height;
3255            let invslope2 = ((p3.x - p1.x) << 16) / height;
3256            let mut cx1 = p1.x << 16;
3257            let mut cx2 = p1.x << 16;
3258            for sy in p1.y..=p2.y {
3259                let dy = sy - p1.y;
3260                let t = dy as f32 / height as f32;
3261                let wl = w1 + t * (w2 - w1);
3262                let wr = w1 + t * (w3 - w1);
3263                let uvl = [
3264                    uv1[0] + t * (uv2[0] - uv1[0]),
3265                    uv1[1] + t * (uv2[1] - uv1[1]),
3266                ];
3267                let uvr = [
3268                    uv1[0] + t * (uv3[0] - uv1[0]),
3269                    uv1[1] + t * (uv3[1] - uv1[1]),
3270                ];
3271                rasterize_span(cx1 >> 16, cx2 >> 16, sy, wl, wr, uvl, uvr, fb, coverage);
3272                cx1 += invslope1;
3273                cx2 += invslope2;
3274            }
3275        };
3276
3277    let draw_flat_top =
3278        |p1: nalgebra::Point2<i32>,
3279         p2: nalgebra::Point2<i32>,
3280         p3: nalgebra::Point2<i32>,
3281         w1: f32,
3282         w2: f32,
3283         w3: f32,
3284         uv1: [f32; 2],
3285         uv2: [f32; 2],
3286         uv3: [f32; 2],
3287         fb: &mut D,
3288         coverage: &mut crate::bsp::coverage::CoverageBuffer<'_>| {
3289            let height = p3.y - p1.y;
3290            if height == 0 {
3291                return;
3292            }
3293            let invslope1 = ((p3.x - p1.x) << 16) / height;
3294            let invslope2 = ((p3.x - p2.x) << 16) / height;
3295            let mut cx1 = p3.x << 16;
3296            let mut cx2 = p3.x << 16;
3297            for sy in (p1.y..=p3.y).rev() {
3298                let dy = sy - p1.y;
3299                let t = dy as f32 / height as f32;
3300                let wl = w1 + t * (w3 - w1);
3301                let wr = w2 + t * (w3 - w2);
3302                let uvl = [
3303                    uv1[0] + t * (uv3[0] - uv1[0]),
3304                    uv1[1] + t * (uv3[1] - uv1[1]),
3305                ];
3306                let uvr = [
3307                    uv2[0] + t * (uv3[0] - uv2[0]),
3308                    uv2[1] + t * (uv3[1] - uv2[1]),
3309                ];
3310                rasterize_span(cx1 >> 16, cx2 >> 16, sy, wl, wr, uvl, uvr, fb, coverage);
3311                cx1 -= invslope1;
3312                cx2 -= invslope2;
3313            }
3314        };
3315
3316    if p2.y == p3.y {
3317        draw_flat_bottom(p1, p2, p3, w1, w2, w3, uv1, uv2, uv3, fb, coverage);
3318    } else if p1.y == p2.y {
3319        draw_flat_top(p1, p2, p3, w1, w2, w3, uv1, uv2, uv3, fb, coverage);
3320    } else {
3321        let dy31 = (p3.y - p1.y) as f32;
3322        let dy21 = (p2.y - p1.y) as f32;
3323        let t = dy21 / dy31;
3324        let p4x = p1.x + ((p3.x - p1.x) as f32 * t) as i32;
3325        let p4 = nalgebra::Point2::new(p4x, p2.y);
3326        let w4 = w1 + (w3 - w1) * t;
3327        let uv4 = [
3328            uv1[0] + (uv3[0] - uv1[0]) * t,
3329            uv1[1] + (uv3[1] - uv1[1]) * t,
3330        ];
3331        draw_flat_bottom(p1, p2, p4, w1, w2, w4, uv1, uv2, uv4, fb, coverage);
3332        draw_flat_top(p2, p4, p3, w2, w4, w3, uv2, uv4, uv3, fb, coverage);
3333    }
3334}
3335
3336#[inline(always)]
3337fn fill_triangle_zbuffered<D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>>(
3338    p1: nalgebra::Point2<i32>,
3339    p2: nalgebra::Point2<i32>,
3340    p3: nalgebra::Point2<i32>,
3341    z1: f32,
3342    z2: f32,
3343    z3: f32,
3344    color: embedded_graphics_core::pixelcolor::Rgb565,
3345    fb: &mut D,
3346    zbuffer: &mut [crate::ZDepth],
3347    width: usize,
3348    fog_config: Option<&FogConfig>,
3349    dither_config: Option<&DitherConfig>,
3350) where
3351    <D as DrawTarget>::Error: Debug,
3352{
3353    // Convert to embedded_graphics Points
3354    let p1_eg = Point::new(p1.x, p1.y);
3355    let p2_eg = Point::new(p2.x, p2.y);
3356    let p3_eg = Point::new(p3.x, p3.y);
3357
3358    // Convert float depths to fixed-point integers (16.16 format)
3359    // This avoids floating-point operations in the inner loop
3360    let z1_int = (z1 * 65536.0) as u32;
3361    let z2_int = (z2 * 65536.0) as u32;
3362    let z3_int = (z3 * 65536.0) as u32;
3363
3364    // Handle flat triangles
3365    if p2_eg.y == p3_eg.y {
3366        fill_bottom_flat_triangle_zbuffered(
3367            p1_eg,
3368            p2_eg,
3369            p3_eg,
3370            z1_int,
3371            z2_int,
3372            z3_int,
3373            color,
3374            fb,
3375            zbuffer,
3376            width,
3377            fog_config,
3378            dither_config,
3379        );
3380    } else if p1_eg.y == p2_eg.y {
3381        fill_top_flat_triangle_zbuffered(
3382            p1_eg,
3383            p2_eg,
3384            p3_eg,
3385            z1_int,
3386            z2_int,
3387            z3_int,
3388            color,
3389            fb,
3390            zbuffer,
3391            width,
3392            fog_config,
3393            dither_config,
3394        );
3395    } else {
3396        // Split into two flat triangles
3397        let t = (p2_eg.y - p1_eg.y) as f32 / (p3_eg.y - p1_eg.y) as f32;
3398        let p4 = Point::new(
3399            (p1_eg.x as f32 + t * (p3_eg.x - p1_eg.x) as f32) as i32,
3400            p2_eg.y,
3401        );
3402        let z4_int = (z1_int as i64 + (t * (z3_int as i64 - z1_int as i64) as f32) as i64) as u32;
3403
3404        fill_bottom_flat_triangle_zbuffered(
3405            p1_eg,
3406            p2_eg,
3407            p4,
3408            z1_int,
3409            z2_int,
3410            z4_int,
3411            color,
3412            fb,
3413            zbuffer,
3414            width,
3415            fog_config,
3416            dither_config,
3417        );
3418        fill_top_flat_triangle_zbuffered(
3419            p2_eg,
3420            p4,
3421            p3_eg,
3422            z2_int,
3423            z4_int,
3424            z3_int,
3425            color,
3426            fb,
3427            zbuffer,
3428            width,
3429            fog_config,
3430            dither_config,
3431        );
3432    }
3433}
3434
3435#[cfg(feature = "lighting")]
3436// Gouraud-shaded triangle with z-buffering
3437#[inline(always)]
3438fn fill_triangle_zbuffered_gouraud<
3439    D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>,
3440>(
3441    p1: nalgebra::Point2<i32>,
3442    p2: nalgebra::Point2<i32>,
3443    p3: nalgebra::Point2<i32>,
3444    z1: f32,
3445    z2: f32,
3446    z3: f32,
3447    c1: embedded_graphics_core::pixelcolor::Rgb565,
3448    c2: embedded_graphics_core::pixelcolor::Rgb565,
3449    c3: embedded_graphics_core::pixelcolor::Rgb565,
3450    fb: &mut D,
3451    zbuffer: &mut [crate::ZDepth],
3452    width: usize,
3453    fog_config: Option<&FogConfig>,
3454    dither_config: Option<&DitherConfig>,
3455) where
3456    <D as DrawTarget>::Error: Debug,
3457{
3458    // Convert to embedded_graphics Points
3459    let p1_eg = Point::new(p1.x, p1.y);
3460    let p2_eg = Point::new(p2.x, p2.y);
3461    let p3_eg = Point::new(p3.x, p3.y);
3462
3463    // Convert float depths to fixed-point integers (16.16 format)
3464    let z1_int = (z1 * 65536.0) as u32;
3465    let z2_int = (z2 * 65536.0) as u32;
3466    let z3_int = (z3 * 65536.0) as u32;
3467
3468    // Handle flat triangles
3469    if p2_eg.y == p3_eg.y {
3470        fill_bottom_flat_triangle_zbuffered_gouraud(
3471            p1_eg,
3472            p2_eg,
3473            p3_eg,
3474            z1_int,
3475            z2_int,
3476            z3_int,
3477            c1,
3478            c2,
3479            c3,
3480            fb,
3481            zbuffer,
3482            width,
3483            fog_config,
3484            dither_config,
3485        );
3486    } else if p1_eg.y == p2_eg.y {
3487        fill_top_flat_triangle_zbuffered_gouraud(
3488            p1_eg,
3489            p2_eg,
3490            p3_eg,
3491            z1_int,
3492            z2_int,
3493            z3_int,
3494            c1,
3495            c2,
3496            c3,
3497            fb,
3498            zbuffer,
3499            width,
3500            fog_config,
3501            dither_config,
3502        );
3503    } else {
3504        // Split into two flat triangles
3505        let t = (p2_eg.y - p1_eg.y) as f32 / (p3_eg.y - p1_eg.y) as f32;
3506        let p4 = Point::new(
3507            (p1_eg.x as f32 + t * (p3_eg.x - p1_eg.x) as f32) as i32,
3508            p2_eg.y,
3509        );
3510        let z4_int = (z1_int as i64 + (t * (z3_int as i64 - z1_int as i64) as f32) as i64) as u32;
3511        let c4 = interpolate_color(c1, c3, t);
3512
3513        fill_bottom_flat_triangle_zbuffered_gouraud(
3514            p1_eg,
3515            p2_eg,
3516            p4,
3517            z1_int,
3518            z2_int,
3519            z4_int,
3520            c1,
3521            c2,
3522            c4,
3523            fb,
3524            zbuffer,
3525            width,
3526            fog_config,
3527            dither_config,
3528        );
3529        fill_top_flat_triangle_zbuffered_gouraud(
3530            p2_eg,
3531            p4,
3532            p3_eg,
3533            z2_int,
3534            z4_int,
3535            z3_int,
3536            c2,
3537            c4,
3538            c3,
3539            fb,
3540            zbuffer,
3541            width,
3542            fog_config,
3543            dither_config,
3544        );
3545    }
3546}
3547
3548#[inline(always)]
3549fn fill_bottom_flat_triangle_zbuffered<
3550    D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>,
3551>(
3552    p1: Point,
3553    p2: Point,
3554    p3: Point,
3555    z1: u32,
3556    z2: u32,
3557    z3: u32,
3558    color: embedded_graphics_core::pixelcolor::Rgb565,
3559    fb: &mut D,
3560    zbuffer: &mut [crate::ZDepth],
3561    width: usize,
3562    fog_config: Option<&FogConfig>,
3563    dither_config: Option<&DitherConfig>,
3564) where
3565    <D as DrawTarget>::Error: Debug,
3566{
3567    let height = p2.y - p1.y;
3568    if height == 0 {
3569        return;
3570    }
3571
3572    // Use fixed-point arithmetic (16.16 format) for edge slopes
3573    // This avoids floating-point operations entirely
3574    let invslope1 = ((p2.x - p1.x) << 16) / height;
3575    let invslope2 = ((p3.x - p1.x) << 16) / height;
3576
3577    let mut curx1 = p1.x << 16; // Fixed-point
3578    let mut curx2 = p1.x << 16; // Fixed-point
3579
3580    // Clamp scanline range to the framebuffer so the loop is always O(height).
3581    // Off-screen rows at the top are skipped by advancing the edge walkers.
3582    let scr_h = (zbuffer.len() / width) as i32;
3583    let y_skip = (0_i32 - p1.y).max(0);
3584    curx1 = curx1.wrapping_add(invslope1.wrapping_mul(y_skip));
3585    curx2 = curx2.wrapping_add(invslope2.wrapping_mul(y_skip));
3586    let y_start = p1.y.max(0);
3587    let y_end = p2.y.min(scr_h - 1);
3588
3589    for scanline_y in y_start..=y_end {
3590        let dy = scanline_y - p1.y;
3591        // Integer interpolation for Z using only integer math
3592        let z_left = if height > 0 {
3593            (z1 as i64 + ((z2 as i64 - z1 as i64) * dy as i64 / height as i64)) as u32
3594        } else {
3595            z1
3596        };
3597        let z_right = if height > 0 {
3598            (z1 as i64 + ((z3 as i64 - z1 as i64) * dy as i64 / height as i64)) as u32
3599        } else {
3600            z1
3601        };
3602
3603        draw_scanline_zbuffered(
3604            curx1 >> 16, // Convert back from fixed-point
3605            curx2 >> 16, // Convert back from fixed-point
3606            scanline_y,
3607            z_left,
3608            z_right,
3609            color,
3610            fb,
3611            zbuffer,
3612            width,
3613            fog_config,
3614            dither_config,
3615        );
3616
3617        curx1 = curx1.wrapping_add(invslope1);
3618        curx2 = curx2.wrapping_add(invslope2);
3619    }
3620}
3621
3622#[inline(always)]
3623fn fill_top_flat_triangle_zbuffered<
3624    D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>,
3625>(
3626    p1: Point,
3627    p2: Point,
3628    p3: Point,
3629    z1: u32,
3630    z2: u32,
3631    z3: u32,
3632    color: embedded_graphics_core::pixelcolor::Rgb565,
3633    fb: &mut D,
3634    zbuffer: &mut [crate::ZDepth],
3635    width: usize,
3636    fog_config: Option<&FogConfig>,
3637    dither_config: Option<&DitherConfig>,
3638) where
3639    <D as DrawTarget>::Error: Debug,
3640{
3641    let height = p3.y - p1.y;
3642    if height == 0 {
3643        return;
3644    }
3645
3646    // Use fixed-point arithmetic (16.16 format) for edge slopes
3647    let invslope1 = ((p3.x - p1.x) << 16) / height;
3648    let invslope2 = ((p3.x - p2.x) << 16) / height;
3649
3650    let mut curx1 = p3.x << 16; // Fixed-point
3651    let mut curx2 = p3.x << 16; // Fixed-point
3652
3653    // Clamp scanline range to the framebuffer so the loop is always O(height).
3654    // Top-flat iterates from p3.y (bottom) upward to p1.y (top), advancing edge
3655    // walkers by subtracting invslope each step.  Skipping off-screen rows at
3656    // the bottom means we've already taken y_skip_bot subtract-steps from p3,
3657    // so we must SUBTRACT y_skip_bot * invslope from the starting position.
3658    let scr_h = (zbuffer.len() / width) as i32;
3659    let y_skip_bot = (p3.y - (scr_h - 1)).max(0);
3660    curx1 = curx1.wrapping_sub(invslope1.wrapping_mul(y_skip_bot));
3661    curx2 = curx2.wrapping_sub(invslope2.wrapping_mul(y_skip_bot));
3662    let y_start = p1.y.max(0);
3663    let y_end = p3.y.min(scr_h - 1);
3664
3665    for scanline_y in (y_start..=y_end).rev() {
3666        let dy = scanline_y - p1.y;
3667        // Integer interpolation for Z using only integer math
3668        let z_left = if height > 0 {
3669            (z1 as i64 + ((z3 as i64 - z1 as i64) * dy as i64 / height as i64)) as u32
3670        } else {
3671            z1
3672        };
3673        let z_right = if height > 0 {
3674            (z2 as i64 + ((z3 as i64 - z2 as i64) * dy as i64 / height as i64)) as u32
3675        } else {
3676            z2
3677        };
3678
3679        draw_scanline_zbuffered(
3680            curx1 >> 16, // Convert back from fixed-point
3681            curx2 >> 16, // Convert back from fixed-point
3682            scanline_y,
3683            z_left,
3684            z_right,
3685            color,
3686            fb,
3687            zbuffer,
3688            width,
3689            fog_config,
3690            dither_config,
3691        );
3692
3693        curx1 = curx1.wrapping_sub(invslope1);
3694        curx2 = curx2.wrapping_sub(invslope2);
3695    }
3696}
3697
3698#[cfg(feature = "lighting")]
3699// Gouraud shaded bottom-flat triangle with z-buffering
3700#[inline(always)]
3701fn fill_bottom_flat_triangle_zbuffered_gouraud<
3702    D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>,
3703>(
3704    p1: Point,
3705    p2: Point,
3706    p3: Point,
3707    z1: u32,
3708    z2: u32,
3709    z3: u32,
3710    c1: embedded_graphics_core::pixelcolor::Rgb565,
3711    c2: embedded_graphics_core::pixelcolor::Rgb565,
3712    c3: embedded_graphics_core::pixelcolor::Rgb565,
3713    fb: &mut D,
3714    zbuffer: &mut [crate::ZDepth],
3715    width: usize,
3716    fog_config: Option<&FogConfig>,
3717    dither_config: Option<&DitherConfig>,
3718) where
3719    <D as DrawTarget>::Error: Debug,
3720{
3721    let height = p2.y - p1.y;
3722    if height == 0 {
3723        return;
3724    }
3725
3726    let invslope1 = ((p2.x - p1.x) << 16) / height;
3727    let invslope2 = ((p3.x - p1.x) << 16) / height;
3728
3729    let mut curx1 = p1.x << 16;
3730    let mut curx2 = p1.x << 16;
3731
3732    for scanline_y in p1.y..=p2.y {
3733        let dy = scanline_y - p1.y;
3734        let t = dy as f32 / height as f32;
3735
3736        // Interpolate Z values
3737        let z_left = if height > 0 {
3738            (z1 as i64 + ((z2 as i64 - z1 as i64) * dy as i64 / height as i64)) as u32
3739        } else {
3740            z1
3741        };
3742        let z_right = if height > 0 {
3743            (z1 as i64 + ((z3 as i64 - z1 as i64) * dy as i64 / height as i64)) as u32
3744        } else {
3745            z1
3746        };
3747
3748        // Interpolate colors
3749        let color_left = interpolate_color(c1, c2, t);
3750        let color_right = interpolate_color(c1, c3, t);
3751
3752        draw_scanline_zbuffered_gouraud(
3753            curx1 >> 16,
3754            curx2 >> 16,
3755            scanline_y,
3756            z_left,
3757            z_right,
3758            color_left,
3759            color_right,
3760            fb,
3761            zbuffer,
3762            width,
3763            fog_config,
3764            dither_config,
3765        );
3766
3767        curx1 += invslope1;
3768        curx2 += invslope2;
3769    }
3770}
3771
3772#[cfg(feature = "lighting")]
3773// Gouraud shaded top-flat triangle with z-buffering
3774#[inline(always)]
3775fn fill_top_flat_triangle_zbuffered_gouraud<
3776    D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>,
3777>(
3778    p1: Point,
3779    p2: Point,
3780    p3: Point,
3781    z1: u32,
3782    z2: u32,
3783    z3: u32,
3784    c1: embedded_graphics_core::pixelcolor::Rgb565,
3785    c2: embedded_graphics_core::pixelcolor::Rgb565,
3786    c3: embedded_graphics_core::pixelcolor::Rgb565,
3787    fb: &mut D,
3788    zbuffer: &mut [crate::ZDepth],
3789    width: usize,
3790    fog_config: Option<&FogConfig>,
3791    dither_config: Option<&DitherConfig>,
3792) where
3793    <D as DrawTarget>::Error: Debug,
3794{
3795    let height = p3.y - p1.y;
3796    if height == 0 {
3797        return;
3798    }
3799
3800    let invslope1 = ((p3.x - p1.x) << 16) / height;
3801    let invslope2 = ((p3.x - p2.x) << 16) / height;
3802
3803    let mut curx1 = p3.x << 16;
3804    let mut curx2 = p3.x << 16;
3805
3806    for scanline_y in (p1.y..=p3.y).rev() {
3807        let dy = scanline_y - p1.y;
3808        let t = dy as f32 / height as f32;
3809
3810        // Interpolate Z values
3811        let z_left = if height > 0 {
3812            (z1 as i64 + ((z3 as i64 - z1 as i64) * dy as i64 / height as i64)) as u32
3813        } else {
3814            z1
3815        };
3816        let z_right = if height > 0 {
3817            (z2 as i64 + ((z3 as i64 - z2 as i64) * dy as i64 / height as i64)) as u32
3818        } else {
3819            z2
3820        };
3821
3822        // Interpolate colors
3823        let color_left = interpolate_color(c1, c3, t);
3824        let color_right = interpolate_color(c2, c3, t);
3825
3826        draw_scanline_zbuffered_gouraud(
3827            curx1 >> 16,
3828            curx2 >> 16,
3829            scanline_y,
3830            z_left,
3831            z_right,
3832            color_left,
3833            color_right,
3834            fb,
3835            zbuffer,
3836            width,
3837            fog_config,
3838            dither_config,
3839        );
3840
3841        curx1 -= invslope1;
3842        curx2 -= invslope2;
3843    }
3844}
3845
3846#[cfg(feature = "lighting")]
3847// Draw scanline with Gouraud shading and z-buffering
3848#[inline(always)]
3849fn draw_scanline_zbuffered_gouraud<
3850    D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>,
3851>(
3852    x1: i32,
3853    x2: i32,
3854    y: i32,
3855    z1: u32,
3856    z2: u32,
3857    color1: embedded_graphics_core::pixelcolor::Rgb565,
3858    color2: embedded_graphics_core::pixelcolor::Rgb565,
3859    fb: &mut D,
3860    zbuffer: &mut [crate::ZDepth],
3861    width: usize,
3862    fog_config: Option<&FogConfig>,
3863    dither_config: Option<&DitherConfig>,
3864) where
3865    <D as DrawTarget>::Error: Debug,
3866{
3867    if y < 0 {
3868        return;
3869    }
3870    let height = zbuffer.len() / width;
3871    if y as usize >= height {
3872        return;
3873    }
3874
3875    let (left_x, right_x, z_left, z_right, c_left, c_right) = if x1 <= x2 {
3876        (x1, x2, z1, z2, color1, color2)
3877    } else {
3878        (x2, x1, z2, z1, color2, color1)
3879    };
3880
3881    let start_x = left_x.max(0);
3882    let end_x = right_x.min(width as i32 - 1);
3883    if start_x > end_x {
3884        return;
3885    }
3886
3887    let span = right_x - left_x;
3888    let inv_span = if span > 0 { 1.0 / span as f32 } else { 0.0 };
3889    let z_step = if span > 0 {
3890        (((z_right as i64 - z_left as i64) << 16) / span as i64) as i32
3891    } else {
3892        0
3893    };
3894
3895    let left_clip = start_x - left_x;
3896    let mut z_curr = ((z_left as i64) << 16) + (left_clip as i64 * z_step as i64);
3897    let mut zbuf_idx = y as usize * width + start_x as usize;
3898
3899    for x in start_x..=end_x {
3900        let z = (z_curr >> 16) as u32;
3901        z_curr += z_step as i64;
3902        let z_depth = crate::to_zdepth(z);
3903
3904        if z_depth < zbuffer[zbuf_idx].saturating_add(crate::DEPTH_EPSILON) {
3905            zbuffer[zbuf_idx] = z_depth;
3906
3907            let t = (x - left_x) as f32 * inv_span;
3908            let mut final_color = interpolate_color(c_left, c_right, t);
3909
3910            if let Some(fog) = fog_config {
3911                final_color = fog.apply(final_color, z);
3912            }
3913
3914            if let Some(dither) = dither_config {
3915                final_color = dither.apply(final_color, x, y);
3916            }
3917
3918            fb.draw_iter([embedded_graphics_core::Pixel(Point::new(x, y), final_color)])
3919                .unwrap();
3920        }
3921        zbuf_idx += 1;
3922    }
3923}
3924
3925#[inline(always)]
3926fn draw_scanline_zbuffered<D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>>(
3927    x1: i32,
3928    x2: i32,
3929    y: i32,
3930    z1: u32,
3931    z2: u32,
3932    color: embedded_graphics_core::pixelcolor::Rgb565,
3933    fb: &mut D,
3934    zbuffer: &mut [crate::ZDepth],
3935    width: usize,
3936    fog_config: Option<&FogConfig>,
3937    dither_config: Option<&DitherConfig>,
3938) where
3939    <D as DrawTarget>::Error: Debug,
3940{
3941    if y < 0 {
3942        return;
3943    }
3944    let height = zbuffer.len() / width;
3945    if y as usize >= height {
3946        return;
3947    }
3948
3949    let (left_x, right_x, z_left, z_right) = if x1 <= x2 {
3950        (x1, x2, z1, z2)
3951    } else {
3952        (x2, x1, z2, z1)
3953    };
3954
3955    let start_x = left_x.max(0);
3956    let end_x = right_x.min(width as i32 - 1);
3957    if start_x > end_x {
3958        return;
3959    }
3960
3961    let span = right_x - left_x;
3962    let z_step = if span > 0 {
3963        (((z_right as i64 - z_left as i64) << 16) / span as i64) as i32
3964    } else {
3965        0
3966    };
3967
3968    let left_clip = start_x - left_x;
3969    let mut z_curr = ((z_left as i64) << 16) + (left_clip as i64 * z_step as i64);
3970    let mut zbuf_idx = y as usize * width + start_x as usize;
3971
3972    for x in start_x..=end_x {
3973        let z = (z_curr >> 16) as u32;
3974        z_curr += z_step as i64;
3975        let z_depth = crate::to_zdepth(z);
3976
3977        if z_depth < zbuffer[zbuf_idx].saturating_add(crate::DEPTH_EPSILON) {
3978            zbuffer[zbuf_idx] = z_depth;
3979
3980            let mut final_color = color;
3981
3982            if let Some(fog) = fog_config {
3983                final_color = fog.apply(final_color, z);
3984            }
3985
3986            if let Some(dither) = dither_config {
3987                final_color = dither.apply(final_color, x, y);
3988            }
3989
3990            fb.draw_iter([embedded_graphics_core::Pixel(Point::new(x, y), final_color)])
3991                .unwrap();
3992        }
3993        zbuf_idx += 1;
3994    }
3995}
3996
3997#[cfg(feature = "textured")]
3998// Textured triangle rendering with z-buffering
3999#[inline(always)]
4000fn fill_triangle_zbuffered_textured<
4001    D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>,
4002>(
4003    p1: nalgebra::Point2<i32>,
4004    p2: nalgebra::Point2<i32>,
4005    p3: nalgebra::Point2<i32>,
4006    z1: f32,
4007    z2: f32,
4008    z3: f32,
4009    w1: f32,
4010    w2: f32,
4011    w3: f32,
4012    uv1: [f32; 2],
4013    uv2: [f32; 2],
4014    uv3: [f32; 2],
4015    texture: &crate::texture::Texture,
4016    fb: &mut D,
4017    zbuffer: &mut [crate::ZDepth],
4018    width: usize,
4019    fog_config: Option<&FogConfig>,
4020    dither_config: Option<&DitherConfig>,
4021    texture_mapping: TextureMapping,
4022    stipple_mode: StippleMode,
4023    screen_tint: Option<ScreenTint>,
4024    palette_mode: PaletteMode,
4025) where
4026    <D as DrawTarget>::Error: Debug,
4027{
4028    // Convert to embedded_graphics Points
4029    let p1_eg = Point::new(p1.x, p1.y);
4030    let p2_eg = Point::new(p2.x, p2.y);
4031    let p3_eg = Point::new(p3.x, p3.y);
4032
4033    // Convert float depths to fixed-point integers (16.16 format)
4034    let z1_int = (z1 * 65536.0) as u32;
4035    let z2_int = (z2 * 65536.0) as u32;
4036    let z3_int = (z3 * 65536.0) as u32;
4037
4038    // Handle flat triangles
4039    if p2_eg.y == p3_eg.y {
4040        fill_bottom_flat_triangle_zbuffered_textured(
4041            p1_eg,
4042            p2_eg,
4043            p3_eg,
4044            z1_int,
4045            z2_int,
4046            z3_int,
4047            w1,
4048            w2,
4049            w3,
4050            uv1,
4051            uv2,
4052            uv3,
4053            texture,
4054            fb,
4055            zbuffer,
4056            width,
4057            fog_config,
4058            dither_config,
4059            texture_mapping,
4060            stipple_mode,
4061            screen_tint,
4062            palette_mode,
4063        );
4064    } else if p1_eg.y == p2_eg.y {
4065        fill_top_flat_triangle_zbuffered_textured(
4066            p1_eg,
4067            p2_eg,
4068            p3_eg,
4069            z1_int,
4070            z2_int,
4071            z3_int,
4072            w1,
4073            w2,
4074            w3,
4075            uv1,
4076            uv2,
4077            uv3,
4078            texture,
4079            fb,
4080            zbuffer,
4081            width,
4082            fog_config,
4083            dither_config,
4084            texture_mapping,
4085            stipple_mode,
4086            screen_tint,
4087            palette_mode,
4088        );
4089    } else {
4090        // Split into two flat triangles
4091        let t = (p2_eg.y - p1_eg.y) as f32 / (p3_eg.y - p1_eg.y) as f32;
4092        let p4 = Point::new(
4093            (p1_eg.x as f32 + t * (p3_eg.x - p1_eg.x) as f32) as i32,
4094            p2_eg.y,
4095        );
4096        let z4_int = (z1_int as i64 + (t * (z3_int as i64 - z1_int as i64) as f32) as i64) as u32;
4097        // Interpolate W at split point
4098        let w4 = w1 + t * (w3 - w1);
4099        // Interpolate UV at split point
4100        let uv4 = [
4101            uv1[0] + t * (uv3[0] - uv1[0]),
4102            uv1[1] + t * (uv3[1] - uv1[1]),
4103        ];
4104
4105        fill_bottom_flat_triangle_zbuffered_textured(
4106            p1_eg,
4107            p2_eg,
4108            p4,
4109            z1_int,
4110            z2_int,
4111            z4_int,
4112            w1,
4113            w2,
4114            w4,
4115            uv1,
4116            uv2,
4117            uv4,
4118            texture,
4119            fb,
4120            zbuffer,
4121            width,
4122            fog_config,
4123            dither_config,
4124            texture_mapping,
4125            stipple_mode,
4126            screen_tint,
4127            palette_mode,
4128        );
4129        fill_top_flat_triangle_zbuffered_textured(
4130            p2_eg,
4131            p4,
4132            p3_eg,
4133            z2_int,
4134            z4_int,
4135            z3_int,
4136            w2,
4137            w4,
4138            w3,
4139            uv2,
4140            uv4,
4141            uv3,
4142            texture,
4143            fb,
4144            zbuffer,
4145            width,
4146            fog_config,
4147            dither_config,
4148            texture_mapping,
4149            stipple_mode,
4150            screen_tint,
4151            palette_mode,
4152        );
4153    }
4154}
4155
4156#[cfg(feature = "textured")]
4157// Textured bottom-flat triangle with z-buffering
4158#[inline(always)]
4159fn fill_bottom_flat_triangle_zbuffered_textured<
4160    D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>,
4161>(
4162    p1: Point,
4163    p2: Point,
4164    p3: Point,
4165    z1: u32,
4166    z2: u32,
4167    z3: u32,
4168    w1: f32,
4169    w2: f32,
4170    w3: f32,
4171    uv1: [f32; 2],
4172    uv2: [f32; 2],
4173    uv3: [f32; 2],
4174    texture: &crate::texture::Texture,
4175    fb: &mut D,
4176    zbuffer: &mut [crate::ZDepth],
4177    width: usize,
4178    fog_config: Option<&FogConfig>,
4179    dither_config: Option<&DitherConfig>,
4180    texture_mapping: TextureMapping,
4181    stipple_mode: StippleMode,
4182    screen_tint: Option<ScreenTint>,
4183    palette_mode: PaletteMode,
4184) where
4185    <D as DrawTarget>::Error: Debug,
4186{
4187    let height = p2.y - p1.y;
4188    if height == 0 {
4189        return;
4190    }
4191
4192    let invslope1 = ((p2.x - p1.x) << 16) / height;
4193    let invslope2 = ((p3.x - p1.x) << 16) / height;
4194
4195    let mut curx1 = p1.x << 16;
4196    let mut curx2 = p1.x << 16;
4197
4198    for scanline_y in p1.y..=p2.y {
4199        let dy = scanline_y - p1.y;
4200        let t = dy as f32 / height as f32;
4201
4202        // Interpolate Z values
4203        let z_left = if height > 0 {
4204            (z1 as i64 + ((z2 as i64 - z1 as i64) * dy as i64 / height as i64)) as u32
4205        } else {
4206            z1
4207        };
4208        let z_right = if height > 0 {
4209            (z1 as i64 + ((z3 as i64 - z1 as i64) * dy as i64 / height as i64)) as u32
4210        } else {
4211            z1
4212        };
4213
4214        // Interpolate W values
4215        let w_left = w1 + t * (w2 - w1);
4216        let w_right = w1 + t * (w3 - w1);
4217
4218        // Interpolate UVs
4219        let uv_left = [
4220            uv1[0] + t * (uv2[0] - uv1[0]),
4221            uv1[1] + t * (uv2[1] - uv1[1]),
4222        ];
4223        let uv_right = [
4224            uv1[0] + t * (uv3[0] - uv1[0]),
4225            uv1[1] + t * (uv3[1] - uv1[1]),
4226        ];
4227
4228        draw_scanline_zbuffered_textured(
4229            curx1 >> 16,
4230            curx2 >> 16,
4231            scanline_y,
4232            z_left,
4233            z_right,
4234            w_left,
4235            w_right,
4236            uv_left,
4237            uv_right,
4238            texture,
4239            fb,
4240            zbuffer,
4241            width,
4242            fog_config,
4243            dither_config,
4244            texture_mapping,
4245            stipple_mode,
4246            screen_tint,
4247            palette_mode,
4248        );
4249
4250        curx1 += invslope1;
4251        curx2 += invslope2;
4252    }
4253}
4254
4255#[cfg(feature = "textured")]
4256// Textured top-flat triangle with z-buffering
4257#[inline(always)]
4258fn fill_top_flat_triangle_zbuffered_textured<
4259    D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>,
4260>(
4261    p1: Point,
4262    p2: Point,
4263    p3: Point,
4264    z1: u32,
4265    z2: u32,
4266    z3: u32,
4267    w1: f32,
4268    w2: f32,
4269    w3: f32,
4270    uv1: [f32; 2],
4271    uv2: [f32; 2],
4272    uv3: [f32; 2],
4273    texture: &crate::texture::Texture,
4274    fb: &mut D,
4275    zbuffer: &mut [crate::ZDepth],
4276    width: usize,
4277    fog_config: Option<&FogConfig>,
4278    dither_config: Option<&DitherConfig>,
4279    texture_mapping: TextureMapping,
4280    stipple_mode: StippleMode,
4281    screen_tint: Option<ScreenTint>,
4282    palette_mode: PaletteMode,
4283) where
4284    <D as DrawTarget>::Error: Debug,
4285{
4286    let height = p3.y - p1.y;
4287    if height == 0 {
4288        return;
4289    }
4290
4291    let invslope1 = ((p3.x - p1.x) << 16) / height;
4292    let invslope2 = ((p3.x - p2.x) << 16) / height;
4293
4294    let mut curx1 = p3.x << 16;
4295    let mut curx2 = p3.x << 16;
4296
4297    for scanline_y in (p1.y..=p3.y).rev() {
4298        let dy = scanline_y - p1.y;
4299        let t = dy as f32 / height as f32;
4300
4301        // Interpolate Z values
4302        let z_left = if height > 0 {
4303            (z1 as i64 + ((z3 as i64 - z1 as i64) * dy as i64 / height as i64)) as u32
4304        } else {
4305            z1
4306        };
4307        let z_right = if height > 0 {
4308            (z2 as i64 + ((z3 as i64 - z2 as i64) * dy as i64 / height as i64)) as u32
4309        } else {
4310            z2
4311        };
4312
4313        // Interpolate W values
4314        let w_left = w1 + t * (w3 - w1);
4315        let w_right = w2 + t * (w3 - w2);
4316
4317        // Interpolate UVs
4318        let uv_left = [
4319            uv1[0] + t * (uv3[0] - uv1[0]),
4320            uv1[1] + t * (uv3[1] - uv1[1]),
4321        ];
4322        let uv_right = [
4323            uv2[0] + t * (uv3[0] - uv2[0]),
4324            uv2[1] + t * (uv3[1] - uv2[1]),
4325        ];
4326
4327        draw_scanline_zbuffered_textured(
4328            curx1 >> 16,
4329            curx2 >> 16,
4330            scanline_y,
4331            z_left,
4332            z_right,
4333            w_left,
4334            w_right,
4335            uv_left,
4336            uv_right,
4337            texture,
4338            fb,
4339            zbuffer,
4340            width,
4341            fog_config,
4342            dither_config,
4343            texture_mapping,
4344            stipple_mode,
4345            screen_tint,
4346            palette_mode,
4347        );
4348
4349        curx1 -= invslope1;
4350        curx2 -= invslope2;
4351    }
4352}
4353
4354#[cfg(feature = "textured")]
4355// Draw scanline with texture mapping and z-buffering
4356#[inline(always)]
4357fn draw_scanline_zbuffered_textured<
4358    D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>,
4359>(
4360    x1: i32,
4361    x2: i32,
4362    y: i32,
4363    z1: u32,
4364    z2: u32,
4365    w1: f32,
4366    w2: f32,
4367    uv1: [f32; 2],
4368    uv2: [f32; 2],
4369    texture: &crate::texture::Texture,
4370    fb: &mut D,
4371    zbuffer: &mut [crate::ZDepth],
4372    width: usize,
4373    fog_config: Option<&FogConfig>,
4374    dither_config: Option<&DitherConfig>,
4375    texture_mapping: TextureMapping,
4376    stipple_mode: StippleMode,
4377    screen_tint: Option<ScreenTint>,
4378    palette_mode: PaletteMode,
4379) where
4380    <D as DrawTarget>::Error: Debug,
4381{
4382    if y < 0 {
4383        return;
4384    }
4385    let height = zbuffer.len() / width;
4386    if y as usize >= height {
4387        return;
4388    }
4389
4390    let (left_x, right_x, z_left, z_right, w_left, w_right, uv_left, uv_right) = if x1 <= x2 {
4391        (x1, x2, z1, z2, w1, w2, uv1, uv2)
4392    } else {
4393        (x2, x1, z2, z1, w2, w1, uv2, uv1)
4394    };
4395
4396    let start_x = left_x.max(0);
4397    let end_x = right_x.min(width as i32 - 1);
4398    if start_x > end_x {
4399        return;
4400    }
4401
4402    let span = right_x - left_x;
4403    let inv_span = if span > 0 { 1.0 / span as f32 } else { 0.0 };
4404    let z_step = if span > 0 {
4405        (((z_right as i64 - z_left as i64) << 16) / span as i64) as i32
4406    } else {
4407        0
4408    };
4409
4410    let left_clip = start_x - left_x;
4411    let mut z_curr = ((z_left as i64) << 16) + (left_clip as i64 * z_step as i64);
4412    let mut zbuf_idx = y as usize * width + start_x as usize;
4413
4414    // Sub-Span Perspective Texture Interpolation:
4415    // Evaluates exact perspective UV division at 16-pixel boundaries and steps linearly within spans.
4416    const SUB_SPAN_SIZE: i32 = 16;
4417
4418    let mut span_x = start_x;
4419    while span_x <= end_x {
4420        let next_span_x = (span_x + SUB_SPAN_SIZE).min(end_x + 1);
4421        let span_len = next_span_x - span_x;
4422
4423        let t_start = (span_x - left_x) as f32 * inv_span;
4424        let t_end = (next_span_x - 1 - left_x) as f32 * inv_span;
4425
4426        let [u_start, v_start] =
4427            interpolate_uv(t_start, w_left, w_right, uv_left, uv_right, texture_mapping);
4428        let [u_end, v_end] =
4429            interpolate_uv(t_end, w_left, w_right, uv_left, uv_right, texture_mapping);
4430
4431        let inv_sub = if span_len > 1 {
4432            1.0 / (span_len - 1) as f32
4433        } else {
4434            0.0
4435        };
4436        let du = (u_end - u_start) * inv_sub;
4437        let dv = (v_end - v_start) * inv_sub;
4438
4439        let mut curr_u = u_start;
4440        let mut curr_v = v_start;
4441
4442        for x in span_x..next_span_x {
4443            if should_skip_stipple(x, y, stipple_mode) {
4444                z_curr += z_step as i64;
4445                zbuf_idx += 1;
4446                curr_u += du;
4447                curr_v += dv;
4448                continue;
4449            }
4450
4451            let z = (z_curr >> 16) as u32;
4452            z_curr += z_step as i64;
4453            let z_depth = crate::to_zdepth(z);
4454
4455            if z_depth < zbuffer[zbuf_idx].saturating_add(crate::DEPTH_EPSILON) {
4456                zbuffer[zbuf_idx] = z_depth;
4457
4458                // Sample texture using sub-span interpolated UVs
4459                let mut final_color = texture.sample(curr_u, curr_v);
4460
4461                // Apply effects in order: fog first, then dithering
4462                if let Some(fog) = fog_config {
4463                    final_color = fog.apply(final_color, z);
4464                }
4465
4466                if let Some(dither) = dither_config {
4467                    final_color = dither.apply(final_color, x, y);
4468                }
4469                if let Some(tint) = screen_tint {
4470                    final_color = tint.apply(final_color);
4471                }
4472                final_color = palette_mode.apply(final_color);
4473
4474                fb.draw_iter([embedded_graphics_core::Pixel(Point::new(x, y), final_color)])
4475                    .unwrap();
4476            }
4477            zbuf_idx += 1;
4478            curr_u += du;
4479            curr_v += dv;
4480        }
4481
4482        span_x = next_span_x;
4483    }
4484}
4485
4486#[inline(always)]
4487fn fill_triangle_zbuffered_translucent<D: DrawTarget<Color = Rgb565>>(
4488    p1: nalgebra::Point2<i32>,
4489    p2: nalgebra::Point2<i32>,
4490    p3: nalgebra::Point2<i32>,
4491    z1: f32,
4492    z2: f32,
4493    z3: f32,
4494    color: Rgb565,
4495    alpha: u8,
4496    fb: &mut D,
4497    zbuffer: &mut [crate::ZDepth],
4498    width: usize,
4499) where
4500    <D as DrawTarget>::Error: Debug,
4501{
4502    let p1_eg = Point::new(p1.x, p1.y);
4503    let p2_eg = Point::new(p2.x, p2.y);
4504    let p3_eg = Point::new(p3.x, p3.y);
4505
4506    let z1_int = (z1 * 65536.0) as u32;
4507    let z2_int = (z2 * 65536.0) as u32;
4508    let z3_int = (z3 * 65536.0) as u32;
4509
4510    if p2_eg.y == p3_eg.y {
4511        fill_bottom_flat_translucent(
4512            p1_eg, p2_eg, p3_eg, z1_int, z2_int, z3_int, color, alpha, fb, zbuffer, width,
4513        );
4514    } else if p1_eg.y == p2_eg.y {
4515        fill_top_flat_translucent(
4516            p1_eg, p2_eg, p3_eg, z1_int, z2_int, z3_int, color, alpha, fb, zbuffer, width,
4517        );
4518    } else {
4519        let t = (p2_eg.y - p1_eg.y) as f32 / (p3_eg.y - p1_eg.y) as f32;
4520        let p4 = Point::new(
4521            (p1_eg.x as f32 + t * (p3_eg.x - p1_eg.x) as f32) as i32,
4522            p2_eg.y,
4523        );
4524        let z4_int = (z1_int as i64 + (t * (z3_int as i64 - z1_int as i64) as f32) as i64) as u32;
4525        fill_bottom_flat_translucent(
4526            p1_eg, p2_eg, p4, z1_int, z2_int, z4_int, color, alpha, fb, zbuffer, width,
4527        );
4528        fill_top_flat_translucent(
4529            p2_eg, p4, p3_eg, z2_int, z4_int, z3_int, color, alpha, fb, zbuffer, width,
4530        );
4531    }
4532}
4533
4534#[inline(always)]
4535fn fill_bottom_flat_translucent<D: DrawTarget<Color = Rgb565>>(
4536    p1: Point,
4537    p2: Point,
4538    p3: Point,
4539    z1: u32,
4540    z2: u32,
4541    z3: u32,
4542    color: Rgb565,
4543    alpha: u8,
4544    fb: &mut D,
4545    zbuffer: &mut [crate::ZDepth],
4546    width: usize,
4547) where
4548    <D as DrawTarget>::Error: Debug,
4549{
4550    let height = p2.y - p1.y;
4551    if height == 0 {
4552        return;
4553    }
4554    let invslope1 = ((p2.x - p1.x) << 16) / height;
4555    let invslope2 = ((p3.x - p1.x) << 16) / height;
4556
4557    let mut curx1 = p1.x << 16;
4558    let mut curx2 = p1.x << 16;
4559
4560    for scanline_y in p1.y..=p2.y {
4561        let dy = scanline_y - p1.y;
4562        let z_left = (z1 as i64 + ((z2 as i64 - z1 as i64) * dy as i64 / height as i64)) as u32;
4563        let z_right = (z1 as i64 + ((z3 as i64 - z1 as i64) * dy as i64 / height as i64)) as u32;
4564
4565        let (left_x, right_x, z_l, z_r) = if curx1 <= curx2 {
4566            (curx1 >> 16, curx2 >> 16, z_left, z_right)
4567        } else {
4568            (curx2 >> 16, curx1 >> 16, z_right, z_left)
4569        };
4570
4571        let span = right_x - left_x;
4572        for x in left_x..=right_x {
4573            if x < 0 || scanline_y < 0 || x >= width as i32 {
4574                continue;
4575            }
4576            let idx = (scanline_y as usize) * width + (x as usize);
4577            if idx >= zbuffer.len() {
4578                continue;
4579            }
4580
4581            let z = if span > 0 {
4582                let t = (x - left_x) as f32 / span as f32;
4583                (z_l as f32 + t * (z_r as f32 - z_l as f32)) as u32
4584            } else {
4585                z_l
4586            };
4587            let z_depth = crate::to_zdepth(z);
4588
4589            if z_depth < zbuffer[idx] {
4590                zbuffer[idx] = z_depth;
4591                let draw_color = fast_blend_rgb565(Rgb565::BLACK, color, alpha);
4592                let _ = fb.draw_iter([embedded_graphics_core::Pixel(
4593                    Point::new(x, scanline_y),
4594                    draw_color,
4595                )]);
4596            }
4597        }
4598
4599        curx1 += invslope1;
4600        curx2 += invslope2;
4601    }
4602}
4603
4604#[inline(always)]
4605fn fill_top_flat_translucent<D: DrawTarget<Color = Rgb565>>(
4606    p1: Point,
4607    p2: Point,
4608    p3: Point,
4609    z1: u32,
4610    z2: u32,
4611    z3: u32,
4612    color: Rgb565,
4613    alpha: u8,
4614    fb: &mut D,
4615    zbuffer: &mut [crate::ZDepth],
4616    width: usize,
4617) where
4618    <D as DrawTarget>::Error: Debug,
4619{
4620    let height = p3.y - p1.y;
4621    if height == 0 {
4622        return;
4623    }
4624    let invslope1 = ((p3.x - p1.x) << 16) / height;
4625    let invslope2 = ((p3.x - p2.x) << 16) / height;
4626
4627    let mut curx1 = p3.x << 16;
4628    let mut curx2 = p3.x << 16;
4629
4630    for scanline_y in (p1.y..=p3.y).rev() {
4631        let dy = scanline_y - p1.y;
4632        let z_left = (z1 as i64 + ((z3 as i64 - z1 as i64) * dy as i64 / height as i64)) as u32;
4633        let z_right = (z2 as i64 + ((z3 as i64 - z2 as i64) * dy as i64 / height as i64)) as u32;
4634
4635        let (left_x, right_x, z_l, z_r) = if curx1 <= curx2 {
4636            (curx1 >> 16, curx2 >> 16, z_left, z_right)
4637        } else {
4638            (curx2 >> 16, curx1 >> 16, z_right, z_left)
4639        };
4640
4641        let span = right_x - left_x;
4642        for x in left_x..=right_x {
4643            if x < 0 || scanline_y < 0 || x >= width as i32 {
4644                continue;
4645            }
4646            let idx = (scanline_y as usize) * width + (x as usize);
4647            if idx >= zbuffer.len() {
4648                continue;
4649            }
4650
4651            let z = if span > 0 {
4652                let t = (x - left_x) as f32 / span as f32;
4653                (z_l as f32 + t * (z_r as f32 - z_l as f32)) as u32
4654            } else {
4655                z_l
4656            };
4657            let z_depth = crate::to_zdepth(z);
4658
4659            if z_depth < zbuffer[idx] {
4660                zbuffer[idx] = z_depth;
4661                let draw_color = fast_blend_rgb565(Rgb565::BLACK, color, alpha);
4662                let _ = fb.draw_iter([embedded_graphics_core::Pixel(
4663                    Point::new(x, scanline_y),
4664                    draw_color,
4665                )]);
4666            }
4667        }
4668
4669        curx1 -= invslope1;
4670        curx2 -= invslope2;
4671    }
4672}
4673
4674#[cfg(feature = "textured")]
4675pub fn fill_triangle_zbuffered_textured_gouraud<
4676    D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>,
4677>(
4678    p1: nalgebra::Point2<i32>,
4679    p2: nalgebra::Point2<i32>,
4680    p3: nalgebra::Point2<i32>,
4681    z1: f32,
4682    z2: f32,
4683    z3: f32,
4684    w1: f32,
4685    w2: f32,
4686    w3: f32,
4687    uv1: [f32; 2],
4688    uv2: [f32; 2],
4689    uv3: [f32; 2],
4690    c1: embedded_graphics_core::pixelcolor::Rgb565,
4691    c2: embedded_graphics_core::pixelcolor::Rgb565,
4692    c3: embedded_graphics_core::pixelcolor::Rgb565,
4693    texture: &crate::texture::Texture,
4694    fb: &mut D,
4695    zbuffer: &mut [crate::ZDepth],
4696    width: usize,
4697    fog_config: Option<&FogConfig>,
4698    dither_config: Option<&DitherConfig>,
4699    texture_mapping: TextureMapping,
4700    stipple_mode: StippleMode,
4701    screen_tint: Option<ScreenTint>,
4702    palette_mode: PaletteMode,
4703) where
4704    <D as DrawTarget>::Error: Debug,
4705{
4706    let p1_eg = Point::new(p1.x, p1.y);
4707    let p2_eg = Point::new(p2.x, p2.y);
4708    let p3_eg = Point::new(p3.x, p3.y);
4709
4710    let z1_int = (z1 * 65536.0) as u32;
4711    let z2_int = (z2 * 65536.0) as u32;
4712    let z3_int = (z3 * 65536.0) as u32;
4713
4714    if p2_eg.y == p3_eg.y {
4715        fill_bottom_flat_triangle_zbuffered_textured_gouraud(
4716            p1_eg,
4717            p2_eg,
4718            p3_eg,
4719            z1_int,
4720            z2_int,
4721            z3_int,
4722            w1,
4723            w2,
4724            w3,
4725            uv1,
4726            uv2,
4727            uv3,
4728            c1,
4729            c2,
4730            c3,
4731            texture,
4732            fb,
4733            zbuffer,
4734            width,
4735            fog_config,
4736            dither_config,
4737            texture_mapping,
4738            stipple_mode,
4739            screen_tint,
4740            palette_mode,
4741        );
4742    } else if p1_eg.y == p2_eg.y {
4743        fill_top_flat_triangle_zbuffered_textured_gouraud(
4744            p1_eg,
4745            p2_eg,
4746            p3_eg,
4747            z1_int,
4748            z2_int,
4749            z3_int,
4750            w1,
4751            w2,
4752            w3,
4753            uv1,
4754            uv2,
4755            uv3,
4756            c1,
4757            c2,
4758            c3,
4759            texture,
4760            fb,
4761            zbuffer,
4762            width,
4763            fog_config,
4764            dither_config,
4765            texture_mapping,
4766            stipple_mode,
4767            screen_tint,
4768            palette_mode,
4769        );
4770    } else {
4771        let t = (p2_eg.y - p1_eg.y) as f32 / (p3_eg.y - p1_eg.y) as f32;
4772        let split_x = (p1_eg.x as f32 + t * (p3_eg.x - p1_eg.x) as f32) as i32;
4773        let p_split = Point::new(split_x, p2_eg.y);
4774
4775        let z_split = (z1_int as f64 + (z3_int as f64 - z1_int as f64) * t as f64) as u32;
4776        let w_split = w1 + t * (w3 - w1);
4777        let uv_split = [
4778            uv1[0] + t * (uv3[0] - uv1[0]),
4779            uv1[1] + t * (uv3[1] - uv1[1]),
4780        ];
4781        let color_split = interpolate_color(c1, c3, t);
4782
4783        fill_bottom_flat_triangle_zbuffered_textured_gouraud(
4784            p1_eg,
4785            p2_eg,
4786            p_split,
4787            z1_int,
4788            z2_int,
4789            z_split,
4790            w1,
4791            w2,
4792            w_split,
4793            uv1,
4794            uv2,
4795            uv_split,
4796            c1,
4797            c2,
4798            color_split,
4799            texture,
4800            fb,
4801            zbuffer,
4802            width,
4803            fog_config,
4804            dither_config,
4805            texture_mapping,
4806            stipple_mode,
4807            screen_tint,
4808            palette_mode,
4809        );
4810
4811        fill_top_flat_triangle_zbuffered_textured_gouraud(
4812            p2_eg,
4813            p_split,
4814            p3_eg,
4815            z2_int,
4816            z_split,
4817            z3_int,
4818            w2,
4819            w_split,
4820            w3,
4821            uv2,
4822            uv_split,
4823            uv3,
4824            c2,
4825            color_split,
4826            c3,
4827            texture,
4828            fb,
4829            zbuffer,
4830            width,
4831            fog_config,
4832            dither_config,
4833            texture_mapping,
4834            stipple_mode,
4835            screen_tint,
4836            palette_mode,
4837        );
4838    }
4839}
4840
4841#[cfg(feature = "textured")]
4842fn fill_bottom_flat_triangle_zbuffered_textured_gouraud<
4843    D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>,
4844>(
4845    p1: Point,
4846    p2: Point,
4847    p3: Point,
4848    z1: u32,
4849    z2: u32,
4850    z3: u32,
4851    w1: f32,
4852    w2: f32,
4853    w3: f32,
4854    uv1: [f32; 2],
4855    uv2: [f32; 2],
4856    uv3: [f32; 2],
4857    c1: Rgb565,
4858    c2: Rgb565,
4859    c3: Rgb565,
4860    texture: &crate::texture::Texture,
4861    fb: &mut D,
4862    zbuffer: &mut [crate::ZDepth],
4863    width: usize,
4864    fog_config: Option<&FogConfig>,
4865    dither_config: Option<&DitherConfig>,
4866    texture_mapping: TextureMapping,
4867    stipple_mode: StippleMode,
4868    screen_tint: Option<ScreenTint>,
4869    palette_mode: PaletteMode,
4870) where
4871    <D as DrawTarget>::Error: Debug,
4872{
4873    let height = p2.y - p1.y;
4874    if height == 0 {
4875        return;
4876    }
4877
4878    let invslope1 = ((p2.x - p1.x) << 16) / height;
4879    let invslope2 = ((p3.x - p1.x) << 16) / height;
4880
4881    let mut curx1 = p1.x << 16;
4882    let mut curx2 = p1.x << 16;
4883
4884    for scanline_y in p1.y..=p2.y {
4885        let dy = scanline_y - p1.y;
4886        let t = dy as f32 / height as f32;
4887
4888        let z_left = if height > 0 {
4889            (z1 as i64 + ((z2 as i64 - z1 as i64) * dy as i64 / height as i64)) as u32
4890        } else {
4891            z1
4892        };
4893        let z_right = if height > 0 {
4894            (z1 as i64 + ((z3 as i64 - z1 as i64) * dy as i64 / height as i64)) as u32
4895        } else {
4896            z1
4897        };
4898
4899        let w_left = w1 + t * (w2 - w1);
4900        let w_right = w1 + t * (w3 - w1);
4901
4902        let uv_left = [
4903            uv1[0] + t * (uv2[0] - uv1[0]),
4904            uv1[1] + t * (uv2[1] - uv1[1]),
4905        ];
4906        let uv_right = [
4907            uv1[0] + t * (uv3[0] - uv1[0]),
4908            uv1[1] + t * (uv3[1] - uv1[1]),
4909        ];
4910
4911        let color_left = interpolate_color(c1, c2, t);
4912        let color_right = interpolate_color(c1, c3, t);
4913
4914        draw_scanline_zbuffered_textured_gouraud(
4915            curx1 >> 16,
4916            curx2 >> 16,
4917            scanline_y,
4918            z_left,
4919            z_right,
4920            w_left,
4921            w_right,
4922            uv_left,
4923            uv_right,
4924            color_left,
4925            color_right,
4926            texture,
4927            fb,
4928            zbuffer,
4929            width,
4930            fog_config,
4931            dither_config,
4932            texture_mapping,
4933            stipple_mode,
4934            screen_tint,
4935            palette_mode,
4936        );
4937
4938        curx1 += invslope1;
4939        curx2 += invslope2;
4940    }
4941}
4942
4943#[cfg(feature = "textured")]
4944fn fill_top_flat_triangle_zbuffered_textured_gouraud<
4945    D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>,
4946>(
4947    p1: Point,
4948    p2: Point,
4949    p3: Point,
4950    z1: u32,
4951    z2: u32,
4952    z3: u32,
4953    w1: f32,
4954    w2: f32,
4955    w3: f32,
4956    uv1: [f32; 2],
4957    uv2: [f32; 2],
4958    uv3: [f32; 2],
4959    c1: Rgb565,
4960    c2: Rgb565,
4961    c3: Rgb565,
4962    texture: &crate::texture::Texture,
4963    fb: &mut D,
4964    zbuffer: &mut [crate::ZDepth],
4965    width: usize,
4966    fog_config: Option<&FogConfig>,
4967    dither_config: Option<&DitherConfig>,
4968    texture_mapping: TextureMapping,
4969    stipple_mode: StippleMode,
4970    screen_tint: Option<ScreenTint>,
4971    palette_mode: PaletteMode,
4972) where
4973    <D as DrawTarget>::Error: Debug,
4974{
4975    let height = p3.y - p1.y;
4976    if height == 0 {
4977        return;
4978    }
4979
4980    let invslope1 = ((p3.x - p1.x) << 16) / height;
4981    let invslope2 = ((p3.x - p2.x) << 16) / height;
4982
4983    let mut curx1 = p3.x << 16;
4984    let mut curx2 = p3.x << 16;
4985
4986    for scanline_y in (p1.y..=p3.y).rev() {
4987        let dy = p3.y - scanline_y;
4988        let t = dy as f32 / height as f32;
4989
4990        let z_left = if height > 0 {
4991            (z3 as i64 + ((z1 as i64 - z3 as i64) * dy as i64 / height as i64)) as u32
4992        } else {
4993            z3
4994        };
4995        let z_right = if height > 0 {
4996            (z3 as i64 + ((z2 as i64 - z3 as i64) * dy as i64 / height as i64)) as u32
4997        } else {
4998            z3
4999        };
5000
5001        let w_left = w3 + t * (w1 - w3);
5002        let w_right = w3 + t * (w2 - w3);
5003
5004        let uv_left = [
5005            uv3[0] + t * (uv1[0] - uv3[0]),
5006            uv3[1] + t * (uv1[1] - uv3[1]),
5007        ];
5008        let uv_right = [
5009            uv3[0] + t * (uv2[0] - uv3[0]),
5010            uv3[1] + t * (uv2[1] - uv3[1]),
5011        ];
5012
5013        let color_left = interpolate_color(c3, c1, t);
5014        let color_right = interpolate_color(c3, c2, t);
5015
5016        draw_scanline_zbuffered_textured_gouraud(
5017            curx1 >> 16,
5018            curx2 >> 16,
5019            scanline_y,
5020            z_left,
5021            z_right,
5022            w_left,
5023            w_right,
5024            uv_left,
5025            uv_right,
5026            color_left,
5027            color_right,
5028            texture,
5029            fb,
5030            zbuffer,
5031            width,
5032            fog_config,
5033            dither_config,
5034            texture_mapping,
5035            stipple_mode,
5036            screen_tint,
5037            palette_mode,
5038        );
5039
5040        curx1 -= invslope1;
5041        curx2 -= invslope2;
5042    }
5043}
5044
5045#[cfg(feature = "textured")]
5046fn draw_scanline_zbuffered_textured_gouraud<
5047    D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>,
5048>(
5049    x1: i32,
5050    x2: i32,
5051    y: i32,
5052    z1: u32,
5053    z2: u32,
5054    w1: f32,
5055    w2: f32,
5056    uv1: [f32; 2],
5057    uv2: [f32; 2],
5058    color1: Rgb565,
5059    color2: Rgb565,
5060    texture: &crate::texture::Texture,
5061    fb: &mut D,
5062    zbuffer: &mut [crate::ZDepth],
5063    width: usize,
5064    fog_config: Option<&FogConfig>,
5065    dither_config: Option<&DitherConfig>,
5066    texture_mapping: TextureMapping,
5067    stipple_mode: StippleMode,
5068    screen_tint: Option<ScreenTint>,
5069    palette_mode: PaletteMode,
5070) where
5071    <D as DrawTarget>::Error: Debug,
5072{
5073    if y < 0 {
5074        return;
5075    }
5076    let height = zbuffer.len() / width;
5077    if y as usize >= height {
5078        return;
5079    }
5080
5081    let (
5082        left_x,
5083        right_x,
5084        z_left,
5085        z_right,
5086        w_left,
5087        w_right,
5088        uv_left,
5089        uv_right,
5090        color_left,
5091        color_right,
5092    ) = if x1 <= x2 {
5093        (x1, x2, z1, z2, w1, w2, uv1, uv2, color1, color2)
5094    } else {
5095        (x2, x1, z2, z1, w2, w1, uv2, uv1, color2, color1)
5096    };
5097
5098    let start_x = left_x.max(0);
5099    let end_x = right_x.min(width as i32 - 1);
5100    if start_x > end_x {
5101        return;
5102    }
5103
5104    let span = right_x - left_x;
5105    let inv_span = if span > 0 { 1.0 / span as f32 } else { 0.0 };
5106    let z_step = if span > 0 {
5107        (((z_right as i64 - z_left as i64) << 16) / span as i64) as i32
5108    } else {
5109        0
5110    };
5111
5112    let left_clip = start_x - left_x;
5113    let mut z_curr = ((z_left as i64) << 16) + (left_clip as i64 * z_step as i64);
5114    let mut zbuf_idx = y as usize * width + start_x as usize;
5115
5116    const SUB_SPAN_SIZE: i32 = 16;
5117    let mut span_x = start_x;
5118
5119    while span_x <= end_x {
5120        let next_span_x = (span_x + SUB_SPAN_SIZE).min(end_x + 1);
5121        let span_len = next_span_x - span_x;
5122
5123        let t_start = (span_x - left_x) as f32 * inv_span;
5124        let t_end = (next_span_x - 1 - left_x) as f32 * inv_span;
5125
5126        let [u_start, v_start] =
5127            interpolate_uv(t_start, w_left, w_right, uv_left, uv_right, texture_mapping);
5128        let [u_end, v_end] =
5129            interpolate_uv(t_end, w_left, w_right, uv_left, uv_right, texture_mapping);
5130
5131        let inv_sub = if span_len > 1 {
5132            1.0 / (span_len - 1) as f32
5133        } else {
5134            0.0
5135        };
5136
5137        let du = (u_end - u_start) * inv_sub;
5138        let dv = (v_end - v_start) * inv_sub;
5139
5140        let mut curr_u = u_start;
5141        let mut curr_v = v_start;
5142
5143        for x in span_x..next_span_x {
5144            if should_skip_stipple(x, y, stipple_mode) {
5145                z_curr += z_step as i64;
5146                zbuf_idx += 1;
5147                curr_u += du;
5148                curr_v += dv;
5149                continue;
5150            }
5151
5152            let z = (z_curr >> 16) as u32;
5153            z_curr += z_step as i64;
5154            let z_depth = crate::to_zdepth(z);
5155
5156            if z_depth < zbuffer[zbuf_idx].saturating_add(crate::DEPTH_EPSILON) {
5157                zbuffer[zbuf_idx] = z_depth;
5158
5159                let tx = (x - left_x) as f32 * inv_span;
5160                let c_interp = interpolate_color(color_left, color_right, tx);
5161                let tex_color = texture.sample(curr_u, curr_v);
5162
5163                let r = ((tex_color.r() as u16 * c_interp.r() as u16) / 31) as u8;
5164                let g = ((tex_color.g() as u16 * c_interp.g() as u16) / 63) as u8;
5165                let b_val = ((tex_color.b() as u16 * c_interp.b() as u16) / 31) as u8;
5166                let mut final_color = Rgb565::new(r, g, b_val);
5167
5168                if let Some(fog) = fog_config {
5169                    final_color = fog.apply(final_color, z);
5170                }
5171                if let Some(dither) = dither_config {
5172                    final_color = dither.apply(final_color, x, y);
5173                }
5174                if let Some(tint) = screen_tint {
5175                    final_color = tint.apply(final_color);
5176                }
5177                final_color = palette_mode.apply(final_color);
5178
5179                fb.draw_iter([embedded_graphics_core::Pixel(Point::new(x, y), final_color)])
5180                    .unwrap();
5181            }
5182            zbuf_idx += 1;
5183            curr_u += du;
5184            curr_v += dv;
5185        }
5186
5187        span_x = next_span_x;
5188    }
5189}
5190
5191#[cfg(test)]
5192mod tests {
5193    extern crate std;
5194    use super::*;
5195    use embedded_graphics_core::pixelcolor::Rgb565;
5196    use embedded_graphics_core::prelude::*;
5197    use nalgebra::Point2;
5198
5199    // Mock framebuffer for testing
5200    struct MockFramebuffer {
5201        pixels: std::vec::Vec<(i32, i32, Rgb565)>,
5202    }
5203
5204    impl MockFramebuffer {
5205        fn new() -> Self {
5206            Self {
5207                pixels: std::vec::Vec::new(),
5208            }
5209        }
5210
5211        fn contains_pixel(&self, x: i32, y: i32) -> bool {
5212            self.pixels.iter().any(|(px, py, _)| *px == x && *py == y)
5213        }
5214
5215        fn pixel_count(&self) -> usize {
5216            self.pixels.len()
5217        }
5218    }
5219
5220    impl DrawTarget for MockFramebuffer {
5221        type Color = Rgb565;
5222        type Error = core::convert::Infallible;
5223
5224        fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
5225        where
5226            I: IntoIterator<Item = embedded_graphics_core::Pixel<Self::Color>>,
5227        {
5228            for pixel in pixels {
5229                self.pixels.push((pixel.0.x, pixel.0.y, pixel.1));
5230            }
5231            Ok(())
5232        }
5233    }
5234
5235    impl OriginDimensions for MockFramebuffer {
5236        fn size(&self) -> Size {
5237            Size::new(640, 480)
5238        }
5239    }
5240
5241    #[test]
5242    fn test_draw_point() {
5243        let mut fb = MockFramebuffer::new();
5244        let point = Point2::new(10, 20);
5245        let color = Rgb565::CSS_RED;
5246
5247        draw(DrawPrimitive::ColoredPoint(point, color), &mut fb);
5248
5249        assert_eq!(fb.pixel_count(), 1);
5250        assert!(fb.contains_pixel(10, 20));
5251    }
5252
5253    #[test]
5254    fn test_draw_line_horizontal() {
5255        let mut fb = MockFramebuffer::new();
5256        let p1 = Point2::new(10, 20);
5257        let p2 = Point2::new(20, 20);
5258        let color = Rgb565::CSS_GREEN;
5259
5260        draw(DrawPrimitive::Line([p1, p2], color), &mut fb);
5261
5262        // Should draw pixels along the horizontal line
5263        assert!(fb.pixel_count() >= 10); // At least 10 pixels
5264        assert!(fb.contains_pixel(10, 20));
5265        assert!(fb.contains_pixel(20, 20));
5266    }
5267
5268    #[test]
5269    fn test_draw_line_vertical() {
5270        let mut fb = MockFramebuffer::new();
5271        let p1 = Point2::new(10, 10);
5272        let p2 = Point2::new(10, 20);
5273        let color = Rgb565::CSS_BLUE;
5274
5275        draw(DrawPrimitive::Line([p1, p2], color), &mut fb);
5276
5277        // Should draw pixels along the vertical line
5278        assert!(fb.pixel_count() >= 10);
5279        assert!(fb.contains_pixel(10, 10));
5280        assert!(fb.contains_pixel(10, 20));
5281    }
5282
5283    #[test]
5284    fn test_draw_line_diagonal() {
5285        let mut fb = MockFramebuffer::new();
5286        let p1 = Point2::new(0, 0);
5287        let p2 = Point2::new(10, 10);
5288        let color = Rgb565::CSS_WHITE;
5289
5290        draw(DrawPrimitive::Line([p1, p2], color), &mut fb);
5291
5292        // Should draw pixels along the diagonal
5293        assert!(fb.pixel_count() >= 10);
5294        assert!(fb.contains_pixel(0, 0));
5295        assert!(fb.contains_pixel(10, 10));
5296    }
5297
5298    #[test]
5299    fn test_draw_triangle_flat_bottom() {
5300        let mut fb = MockFramebuffer::new();
5301        let vertices = [
5302            Point2::new(50, 10), // Top vertex
5303            Point2::new(30, 30), // Bottom left
5304            Point2::new(70, 30), // Bottom right
5305        ];
5306        let color = Rgb565::CSS_YELLOW;
5307
5308        draw(DrawPrimitive::ColoredTriangle(vertices, color), &mut fb);
5309
5310        // Should draw multiple pixels for the filled triangle
5311        let count = fb.pixel_count();
5312        assert!(count > 0, "Expected pixels to be drawn, got {}", count);
5313        // Top vertex should be drawn
5314        assert!(fb.contains_pixel(50, 10));
5315    }
5316
5317    #[test]
5318    fn test_draw_triangle_flat_top() {
5319        let mut fb = MockFramebuffer::new();
5320        let vertices = [
5321            Point2::new(30, 10), // Top left
5322            Point2::new(70, 10), // Top right
5323            Point2::new(50, 30), // Bottom vertex
5324        ];
5325        let color = Rgb565::CSS_CYAN;
5326
5327        draw(DrawPrimitive::ColoredTriangle(vertices, color), &mut fb);
5328
5329        // Should draw multiple pixels for the filled triangle
5330        assert!(fb.pixel_count() > 20);
5331        assert!(fb.contains_pixel(50, 30));
5332    }
5333
5334    #[test]
5335    fn test_draw_triangle_general() {
5336        let mut fb = MockFramebuffer::new();
5337        let vertices = [
5338            Point2::new(50, 10),
5339            Point2::new(30, 30),
5340            Point2::new(80, 40),
5341        ];
5342        let color = Rgb565::CSS_MAGENTA;
5343
5344        draw(DrawPrimitive::ColoredTriangle(vertices, color), &mut fb);
5345
5346        // Should draw many pixels for the filled triangle
5347        assert!(fb.pixel_count() > 30);
5348    }
5349
5350    #[test]
5351    fn test_triangle_vertex_sorting() {
5352        let mut fb = MockFramebuffer::new();
5353        // Vertices in reverse y order
5354        let vertices = [
5355            Point2::new(50, 30), // Bottom (will be sorted to top)
5356            Point2::new(30, 10), // Top
5357            Point2::new(70, 20), // Middle
5358        ];
5359        let color = Rgb565::CSS_WHITE;
5360
5361        // Should not panic and should draw the triangle correctly
5362        draw(DrawPrimitive::ColoredTriangle(vertices, color), &mut fb);
5363
5364        assert!(fb.pixel_count() > 10);
5365    }
5366
5367    #[test]
5368    fn test_draw_multiple_primitives() {
5369        let mut fb = MockFramebuffer::new();
5370
5371        draw(
5372            DrawPrimitive::ColoredPoint(Point2::new(5, 5), Rgb565::CSS_RED),
5373            &mut fb,
5374        );
5375        draw(
5376            DrawPrimitive::Line(
5377                [Point2::new(10, 10), Point2::new(20, 20)],
5378                Rgb565::CSS_GREEN,
5379            ),
5380            &mut fb,
5381        );
5382
5383        // Should have pixels from both primitives
5384        assert!(fb.pixel_count() > 11); // 1 point + at least 10 from line
5385        assert!(fb.contains_pixel(5, 5));
5386    }
5387
5388    #[test]
5389    fn test_scanline_z_linear_interpolation_correctness() {
5390        let width = 100;
5391        let mut zbuffer = std::vec![crate::Z_MAX_VALUE; width * 10];
5392        let mut fb = MockFramebuffer::new();
5393
5394        let x1 = 10;
5395        let x2 = 90;
5396        let y = 5;
5397        let z1 = 10000u32;
5398        let z2 = 90000u32;
5399
5400        draw_scanline_zbuffered(
5401            x1,
5402            x2,
5403            y,
5404            z1,
5405            z2,
5406            Rgb565::CSS_BLUE,
5407            &mut fb,
5408            &mut zbuffer,
5409            width,
5410            None,
5411            None,
5412        );
5413
5414        // Verify that Z buffer contains linearly interpolated values across x=10..=90
5415        let span = (x2 - x1) as f64;
5416        for x in x1..=x2 {
5417            let idx = y as usize * width + x as usize;
5418            let actual_z = zbuffer[idx];
5419            let expected_z = (z1 as f64 + (x - x1) as f64 * (z2 - z1) as f64 / span) as u32;
5420            let expected_z_depth = crate::to_zdepth(expected_z);
5421
5422            let diff = (actual_z as i64 - expected_z_depth as i64).abs();
5423            assert!(
5424                diff <= 2,
5425                "Z interpolation error at x={}: actual={}, expected={}, diff={}",
5426                x,
5427                actual_z,
5428                expected_z_depth,
5429                diff
5430            );
5431        }
5432    }
5433
5434    #[test]
5435    fn test_zbuffer_depth_occlusion_correctness() {
5436        let width = 50;
5437        let mut zbuffer = std::vec![crate::Z_MAX_VALUE; width * 5];
5438        let mut fb = MockFramebuffer::new();
5439
5440        // Draw a far scanline at z = 40000<<16
5441        draw_scanline_zbuffered(
5442            10,
5443            20,
5444            2,
5445            40000 << 16,
5446            40000 << 16,
5447            Rgb565::CSS_RED,
5448            &mut fb,
5449            &mut zbuffer,
5450            width,
5451            None,
5452            None,
5453        );
5454
5455        // Draw a closer scanline at z = 20000<<16 over the same span
5456        draw_scanline_zbuffered(
5457            10,
5458            20,
5459            2,
5460            20000 << 16,
5461            20000 << 16,
5462            Rgb565::CSS_GREEN,
5463            &mut fb,
5464            &mut zbuffer,
5465            width,
5466            None,
5467            None,
5468        );
5469
5470        // All Z values should now be 20000<<16 depth
5471        for x in 10..=20 {
5472            let idx = 2 * width + x;
5473            assert_eq!(zbuffer[idx], crate::to_zdepth(20000 << 16));
5474        }
5475
5476        // Draw a farther scanline at z = 60000<<16 over the same span (should be culled)
5477        draw_scanline_zbuffered(
5478            10,
5479            20,
5480            2,
5481            60000 << 16,
5482            60000 << 16,
5483            Rgb565::CSS_BLUE,
5484            &mut fb,
5485            &mut zbuffer,
5486            width,
5487            None,
5488            None,
5489        );
5490
5491        // Z values must remain 20000<<16 (not overwritten by 60000<<16)
5492        for x in 10..=20 {
5493            let idx = 2 * width + x;
5494            assert_eq!(zbuffer[idx], crate::to_zdepth(20000 << 16));
5495        }
5496    }
5497
5498    #[test]
5499    #[cfg(feature = "textured")]
5500    fn test_sub_span_textured_scanline_correctness() {
5501        let width = 100;
5502        let mut zbuffer = std::vec![crate::Z_MAX_VALUE; width * 10];
5503        let mut fb = MockFramebuffer::new();
5504
5505        // Create a 2x2 static texture with distinct colors
5506        static TEX_DATA: [Rgb565; 4] = [
5507            Rgb565::CSS_RED,
5508            Rgb565::CSS_GREEN,
5509            Rgb565::CSS_BLUE,
5510            Rgb565::CSS_YELLOW,
5511        ];
5512        let texture = crate::texture::Texture::new(&TEX_DATA, 2, 2);
5513
5514        // Draw a 64-pixel scanline across multiple 16-pixel sub-spans (x=10..73)
5515        draw_scanline_zbuffered_textured(
5516            10,
5517            73,
5518            4,
5519            1000 << 16,
5520            1000 << 16,
5521            1.0,
5522            1.0,
5523            [0.0, 0.0],
5524            [1.0, 1.0],
5525            &texture,
5526            &mut fb,
5527            &mut zbuffer,
5528            width,
5529            None,
5530            None,
5531            TextureMapping::Affine,
5532            StippleMode::Off,
5533            None,
5534            PaletteMode::Off,
5535        );
5536
5537        // Every pixel from x=10 to 73 must be rendered and zbuffer updated
5538        for x in 10..=73 {
5539            let idx = 4 * width + x as usize;
5540            assert_eq!(zbuffer[idx], crate::to_zdepth(1000 << 16));
5541        }
5542        assert!(fb.pixel_count() >= 64);
5543    }
5544
5545    #[test]
5546    fn test_fast_blend_rgb565() {
5547        let bg = Rgb565::BLACK;
5548        let fg = Rgb565::WHITE;
5549        assert_eq!(fast_blend_rgb565(bg, fg, 0), bg);
5550        assert_eq!(fast_blend_rgb565(bg, fg, 255), fg);
5551
5552        let blended = fast_blend_rgb565(bg, fg, 128);
5553        assert!(blended.r() > 0 && blended.r() < 31);
5554    }
5555
5556    #[test]
5557    fn test_fast_blend_rgba8888() {
5558        let bg = [0, 0, 0, 255];
5559        let fg = [255, 255, 255, 128];
5560        let out = fast_blend_rgba8888(bg, fg);
5561        assert!(out[0] > 0 && out[0] < 255);
5562    }
5563
5564    #[test]
5565    fn test_fast_blend_rgba8888_to_rgb565() {
5566        let bg = Rgb565::BLACK;
5567        let fg = [255, 0, 0, 255];
5568        let out = fast_blend_rgba8888_to_rgb565(bg, fg);
5569        assert_eq!(out, Rgb565::CSS_RED);
5570    }
5571}
5572
5573#[cfg(feature = "fixed-raster")]
5574pub fn fill_triangle_fixed<D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>>(
5575    mut p1: Point,
5576    mut p2: Point,
5577    mut p3: Point,
5578    color: embedded_graphics_core::pixelcolor::Rgb565,
5579    fb: &mut D,
5580) where
5581    <D as DrawTarget>::Error: Debug,
5582{
5583    if p1.y > p2.y {
5584        core::mem::swap(&mut p1, &mut p2);
5585    }
5586    if p1.y > p3.y {
5587        core::mem::swap(&mut p1, &mut p3);
5588    }
5589    if p2.y > p3.y {
5590        core::mem::swap(&mut p2, &mut p3);
5591    }
5592
5593    if p1.y == p3.y {
5594        return;
5595    }
5596
5597    let bounds = fb.bounding_box();
5598    let min_x = bounds.top_left.x;
5599    let max_x = bounds.bottom_right().unwrap().x;
5600    let min_y = bounds.top_left.y;
5601    let max_y = bounds.bottom_right().unwrap().y;
5602
5603    let dy12 = p2.y - p1.y;
5604    let dy13 = p3.y - p1.y;
5605    let dy23 = p3.y - p2.y;
5606
5607    let dx12_step = if dy12 > 0 {
5608        ((p2.x - p1.x) << 16) / dy12
5609    } else {
5610        0
5611    };
5612    let dx13_step = if dy13 > 0 {
5613        ((p3.x - p1.x) << 16) / dy13
5614    } else {
5615        0
5616    };
5617    let dx23_step = if dy23 > 0 {
5618        ((p3.x - p2.x) << 16) / dy23
5619    } else {
5620        0
5621    };
5622
5623    let mut x13_fp = (p1.x << 16) + 0x8000;
5624    let mut x12_fp = x13_fp;
5625
5626    for y in p1.y..p2.y {
5627        if y >= min_y && y <= max_y {
5628            let xa = (x12_fp >> 16).clamp(min_x, max_x);
5629            let xb = (x13_fp >> 16).clamp(min_x, max_x);
5630            let (start_x, end_x) = if xa <= xb { (xa, xb) } else { (xb, xa) };
5631            for x in start_x..=end_x {
5632                let _ = fb.draw_iter(core::iter::once(embedded_graphics_core::Pixel(
5633                    Point::new(x, y),
5634                    color,
5635                )));
5636            }
5637        }
5638        x12_fp += dx12_step;
5639        x13_fp += dx13_step;
5640    }
5641
5642    let mut x23_fp = (p2.x << 16) + 0x8000;
5643    for y in p2.y..=p3.y {
5644        if y >= min_y && y <= max_y {
5645            let xa = (x23_fp >> 16).clamp(min_x, max_x);
5646            let xb = (x13_fp >> 16).clamp(min_x, max_x);
5647            let (start_x, end_x) = if xa <= xb { (xa, xb) } else { (xb, xa) };
5648            for x in start_x..=end_x {
5649                let _ = fb.draw_iter(core::iter::once(embedded_graphics_core::Pixel(
5650                    Point::new(x, y),
5651                    color,
5652                )));
5653            }
5654        }
5655        x23_fp += dx23_step;
5656        x13_fp += dx13_step;
5657    }
5658}
5659
5660#[cfg(feature = "fixed-raster")]
5661pub fn fill_triangle_zbuffered_fixed<
5662    D: DrawTarget<Color = embedded_graphics_core::pixelcolor::Rgb565>,
5663>(
5664    mut p1: Point,
5665    mut p2: Point,
5666    mut p3: Point,
5667    mut z1: u32,
5668    mut z2: u32,
5669    mut z3: u32,
5670    color: embedded_graphics_core::pixelcolor::Rgb565,
5671    fb: &mut D,
5672    zbuffer: &mut [crate::ZDepth],
5673    width: usize,
5674) where
5675    <D as DrawTarget>::Error: Debug,
5676{
5677    if p1.y > p2.y {
5678        core::mem::swap(&mut p1, &mut p2);
5679        core::mem::swap(&mut z1, &mut z2);
5680    }
5681    if p1.y > p3.y {
5682        core::mem::swap(&mut p1, &mut p3);
5683        core::mem::swap(&mut z1, &mut z3);
5684    }
5685    if p2.y > p3.y {
5686        core::mem::swap(&mut p2, &mut p3);
5687        core::mem::swap(&mut z2, &mut z3);
5688    }
5689
5690    if p1.y == p3.y {
5691        return;
5692    }
5693
5694    let bounds = fb.bounding_box();
5695    let min_x = bounds.top_left.x;
5696    let max_x = bounds.bottom_right().unwrap().x;
5697    let min_y = bounds.top_left.y;
5698    let max_y = bounds.bottom_right().unwrap().y;
5699
5700    let dy12 = p2.y - p1.y;
5701    let dy13 = p3.y - p1.y;
5702    let dy23 = p3.y - p2.y;
5703
5704    let dx12_step = if dy12 > 0 {
5705        ((p2.x - p1.x) << 16) / dy12
5706    } else {
5707        0
5708    };
5709    let dx13_step = if dy13 > 0 {
5710        ((p3.x - p1.x) << 16) / dy13
5711    } else {
5712        0
5713    };
5714    let dx23_step = if dy23 > 0 {
5715        ((p3.x - p2.x) << 16) / dy23
5716    } else {
5717        0
5718    };
5719
5720    let dz12_step = if dy12 > 0 {
5721        ((z2 as i64 - z1 as i64) << 16) / dy12 as i64
5722    } else {
5723        0
5724    };
5725    let dz13_step = if dy13 > 0 {
5726        ((z3 as i64 - z1 as i64) << 16) / dy13 as i64
5727    } else {
5728        0
5729    };
5730    let dz23_step = if dy23 > 0 {
5731        ((z3 as i64 - z2 as i64) << 16) / dy23 as i64
5732    } else {
5733        0
5734    };
5735
5736    let mut x13_fp = (p1.x << 16) + 0x8000;
5737    let mut x12_fp = x13_fp;
5738    let mut z13_fp = (z1 as i64) << 16;
5739    let mut z12_fp = z13_fp;
5740
5741    for y in p1.y..p2.y {
5742        if y >= min_y && y <= max_y {
5743            let xa = x12_fp >> 16;
5744            let xb = x13_fp >> 16;
5745            let (start_x, end_x, za_fp, zb_fp) = if xa <= xb {
5746                (xa, xb, z12_fp, z13_fp)
5747            } else {
5748                (xb, xa, z13_fp, z12_fp)
5749            };
5750            let span_dx = end_x - start_x;
5751            let dz_span_step = if span_dx > 0 {
5752                (zb_fp - za_fp) / span_dx as i64
5753            } else {
5754                0
5755            };
5756            let mut z_curr_fp = za_fp;
5757
5758            for x in start_x..=end_x {
5759                if x >= min_x && x <= max_x {
5760                    let z_val = (z_curr_fp >> 16) as u32;
5761                    let zdepth = crate::to_zdepth(z_val);
5762                    let idx = (y as usize) * width + (x as usize);
5763                    if idx < zbuffer.len() && zdepth < zbuffer[idx] {
5764                        zbuffer[idx] = zdepth;
5765                        let _ = fb.draw_iter(core::iter::once(embedded_graphics_core::Pixel(
5766                            Point::new(x, y),
5767                            color,
5768                        )));
5769                    }
5770                }
5771                z_curr_fp += dz_span_step;
5772            }
5773        }
5774        x12_fp += dx12_step;
5775        x13_fp += dx13_step;
5776        z12_fp += dz12_step;
5777        z13_fp += dz13_step;
5778    }
5779
5780    let mut x23_fp = ((p1.x << 16) + 0x8000) + dx12_step * dy12;
5781    let mut z23_fp = ((z1 as i64) << 16) + dz12_step * dy12 as i64;
5782    for y in p2.y..=p3.y {
5783        if y >= min_y && y <= max_y {
5784            let xa = x23_fp >> 16;
5785            let xb = x13_fp >> 16;
5786            let (start_x, end_x, za_fp, zb_fp) = if xa <= xb {
5787                (xa, xb, z23_fp, z13_fp)
5788            } else {
5789                (xb, xa, z13_fp, z23_fp)
5790            };
5791            let span_dx = end_x - start_x;
5792            let dz_span_step = if span_dx > 0 {
5793                (zb_fp - za_fp) / span_dx as i64
5794            } else {
5795                0
5796            };
5797            let mut z_curr_fp = za_fp;
5798
5799            for x in start_x..=end_x {
5800                if x >= min_x && x <= max_x {
5801                    let z_val = (z_curr_fp >> 16) as u32;
5802                    let zdepth = crate::to_zdepth(z_val);
5803                    let idx = (y as usize) * width + (x as usize);
5804                    if idx < zbuffer.len() && zdepth < zbuffer[idx] {
5805                        zbuffer[idx] = zdepth;
5806                        let _ = fb.draw_iter(core::iter::once(embedded_graphics_core::Pixel(
5807                            Point::new(x, y),
5808                            color,
5809                        )));
5810                    }
5811                }
5812                z_curr_fp += dz_span_step;
5813            }
5814        }
5815        x23_fp += dx23_step;
5816        x13_fp += dx13_step;
5817        z23_fp += dz23_step;
5818        z13_fp += dz13_step;
5819    }
5820}