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