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