Skip to main content

embedded_3dgfx/
renderer.rs

1use core::fmt::Debug;
2
3use embedded_graphics_core::{
4    Pixel,
5    draw_target::DrawTarget,
6    pixelcolor::{Rgb565, RgbColor},
7    prelude::{OriginDimensions, Point},
8};
9
10use crate::{
11    command_buffer::{CommandBuffer, RenderCommand},
12    draw::{DitherConfig, FogConfig, draw_zbuffered_with_effects},
13    error::{BudgetKind, RenderError},
14    primitive::DrawPrimitive,
15    retro::{PaletteMode, ScreenTint, SkyConfig, StippleMode},
16};
17
18pub struct FrameCtx<'a> {
19    pub zbuffer: &'a mut [crate::ZDepth],
20    pub width: usize,
21    pub height: usize,
22}
23
24impl<'a> FrameCtx<'a> {
25    pub fn validate(&self) -> Result<(), RenderError> {
26        let expected = self.width * self.height;
27        if self.zbuffer.len() != expected {
28            return Err(RenderError::OutOfBudget(BudgetKind::ZBufferLength {
29                expected,
30                got: self.zbuffer.len(),
31            }));
32        }
33        Ok(())
34    }
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub struct DirtyRegion {
39    pub x: usize,
40    pub y: usize,
41    pub width: usize,
42    pub height: usize,
43}
44
45impl DirtyRegion {
46    fn from_bounds(min_x: i32, min_y: i32, max_x: i32, max_y: i32) -> Option<Self> {
47        if max_x < min_x || max_y < min_y {
48            return None;
49        }
50        Some(Self {
51            x: min_x as usize,
52            y: min_y as usize,
53            width: (max_x - min_x + 1) as usize,
54            height: (max_y - min_y + 1) as usize,
55        })
56    }
57}
58
59/// A screen-space pick query asking for the topmost hit at pixel `(x, y)`.
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub struct PickQuery {
62    pub x: i32,
63    pub y: i32,
64}
65
66impl PickQuery {
67    pub const fn new(x: i32, y: i32) -> Self {
68        Self { x, y }
69    }
70}
71
72/// Result of an integrated screen-space pick query during command execution.
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub struct PickResult {
75    /// Screen X coordinate tested.
76    pub x: i32,
77    /// Screen Y coordinate tested.
78    pub y: i32,
79    /// Closest depth recorded at this pixel.
80    pub depth: crate::ZDepth,
81    /// Index of the command in the CommandBuffer that hit this pixel.
82    pub command_index: usize,
83}
84
85#[inline(always)]
86fn primitive_bounds(primitive: &DrawPrimitive) -> (i32, i32, i32, i32) {
87    primitive.bounds()
88}
89
90fn clamp_bounds_to_frame(
91    min_x: i32,
92    min_y: i32,
93    max_x: i32,
94    max_y: i32,
95    width: usize,
96    height: usize,
97) -> Option<(i32, i32, i32, i32)> {
98    let w = width as i32;
99    let h = height as i32;
100    let clamped_min_x = min_x.clamp(0, w.saturating_sub(1));
101    let clamped_min_y = min_y.clamp(0, h.saturating_sub(1));
102    let clamped_max_x = max_x.clamp(0, w.saturating_sub(1));
103    let clamped_max_y = max_y.clamp(0, h.saturating_sub(1));
104    if clamped_max_x < clamped_min_x || clamped_max_y < clamped_min_y {
105        return None;
106    }
107    Some((clamped_min_x, clamped_min_y, clamped_max_x, clamped_max_y))
108}
109
110#[inline]
111fn apply_post(color: Rgb565, tint: Option<ScreenTint>, palette_mode: PaletteMode) -> Rgb565 {
112    let tinted = if let Some(t) = tint {
113        t.apply(color)
114    } else {
115        color
116    };
117    palette_mode.apply(tinted)
118}
119
120fn tint_primitive(
121    primitive: &DrawPrimitive,
122    tint: Option<ScreenTint>,
123    palette_mode: PaletteMode,
124) -> DrawPrimitive {
125    match primitive.clone() {
126        DrawPrimitive::ColoredPoint(p, color) => {
127            DrawPrimitive::ColoredPoint(p, apply_post(color, tint, palette_mode))
128        }
129        DrawPrimitive::Line(points, color) => {
130            DrawPrimitive::Line(points, apply_post(color, tint, palette_mode))
131        }
132        DrawPrimitive::ColoredTriangle(points, color) => {
133            DrawPrimitive::ColoredTriangle(points, apply_post(color, tint, palette_mode))
134        }
135        DrawPrimitive::ColoredTriangleWithDepth {
136            points,
137            depths,
138            color,
139        } => DrawPrimitive::ColoredTriangleWithDepth {
140            points,
141            depths,
142            color: apply_post(color, tint, palette_mode),
143        },
144        DrawPrimitive::TranslucentTriangleWithDepth {
145            points,
146            depths,
147            color,
148            alpha,
149        } => DrawPrimitive::TranslucentTriangleWithDepth {
150            points,
151            depths,
152            color: apply_post(color, tint, palette_mode),
153            alpha,
154        },
155        DrawPrimitive::ScreenDoorTriangleWithDepth {
156            points,
157            depths,
158            color,
159            alpha,
160        } => DrawPrimitive::ScreenDoorTriangleWithDepth {
161            points,
162            depths,
163            color: apply_post(color, tint, palette_mode),
164            alpha,
165        },
166        #[cfg(feature = "lighting")]
167        DrawPrimitive::GouraudTriangle { points, colors } => DrawPrimitive::GouraudTriangle {
168            points,
169            colors: [
170                apply_post(colors[0], tint, palette_mode),
171                apply_post(colors[1], tint, palette_mode),
172                apply_post(colors[2], tint, palette_mode),
173            ],
174        },
175        #[cfg(feature = "lighting")]
176        DrawPrimitive::GouraudTriangleWithDepth {
177            points,
178            depths,
179            colors,
180        } => DrawPrimitive::GouraudTriangleWithDepth {
181            points,
182            depths,
183            colors: [
184                apply_post(colors[0], tint, palette_mode),
185                apply_post(colors[1], tint, palette_mode),
186                apply_post(colors[2], tint, palette_mode),
187            ],
188        },
189        #[cfg(feature = "textured")]
190        DrawPrimitive::LightmappedTriangle {
191            points,
192            depths,
193            ws,
194            surface_uvs,
195            lm_uvs,
196            texture_id,
197            lightmap_id,
198            brightness,
199            dynamic_tint,
200        } => DrawPrimitive::LightmappedTriangle {
201            points,
202            depths,
203            ws,
204            surface_uvs,
205            lm_uvs,
206            texture_id,
207            lightmap_id,
208            brightness,
209            dynamic_tint: apply_post(dynamic_tint, tint, palette_mode),
210        },
211        #[cfg(feature = "textured")]
212        DrawPrimitive::TexturedGouraudTriangleWithDepth {
213            points,
214            depths,
215            ws,
216            uvs,
217            colors,
218            texture_id,
219        } => DrawPrimitive::TexturedGouraudTriangleWithDepth {
220            points,
221            depths,
222            ws,
223            uvs,
224            colors: [
225                apply_post(colors[0], tint, palette_mode),
226                apply_post(colors[1], tint, palette_mode),
227                apply_post(colors[2], tint, palette_mode),
228            ],
229            texture_id,
230        },
231        #[cfg(feature = "textured")]
232        other => other,
233    }
234}
235
236#[inline]
237fn blend_rgb565(a: Rgb565, b: Rgb565, t_q8: u16) -> Rgb565 {
238    let inv = 255u16.saturating_sub(t_q8);
239    let r = ((a.r() as u16 * inv + b.r() as u16 * t_q8) / 255) as u8;
240    let g = ((a.g() as u16 * inv + b.g() as u16 * t_q8) / 255) as u8;
241    let bch = ((a.b() as u16 * inv + b.b() as u16 * t_q8) / 255) as u8;
242    Rgb565::new(r, g, bch)
243}
244
245#[inline]
246fn stripe_on_at(x: i32, scroll: i32, stripe_w: i32) -> bool {
247    (((x + scroll).div_euclid(stripe_w)) & 1) == 0
248}
249
250fn draw_sky_background<D>(
251    fb: &mut D,
252    width: usize,
253    height: usize,
254    sky: SkyConfig,
255    camera_dir: [f32; 3],
256    screen_tint: Option<ScreenTint>,
257    palette_mode: PaletteMode,
258) -> Result<(), RenderError>
259where
260    D: DrawTarget<Color = Rgb565> + OriginDimensions,
261    D::Error: Debug,
262{
263    let w = width as i32;
264    let h = height as i32;
265    if w <= 0 || h <= 0 {
266        return Ok(());
267    }
268
269    let horizon = (h as f32 * (0.5 + camera_dir[1].clamp(-1.0, 1.0) * 0.25)) as i32;
270    let stripe_w = sky.stripe_width.max(1) as i32;
271    let scroll = (camera_dir[0] * 128.0) as i32;
272    let stripe_fade_span = (h / 6).max(1);
273
274    for y in 0..h {
275        let dy = (y - horizon + h / 2).clamp(0, h);
276        let t_q8 = ((dy as i64 * 255) / h.max(1) as i64) as u16;
277        let base = blend_rgb565(sky.top_color, sky.bottom_color, t_q8);
278        let below_horizon = (y - horizon).max(0);
279        let stripe_strength = if below_horizon == 0 {
280            sky.stripe_strength as u16
281        } else if below_horizon >= stripe_fade_span {
282            0
283        } else {
284            let rem = stripe_fade_span - below_horizon;
285            ((sky.stripe_strength as i32 * rem) / stripe_fade_span) as u16
286        };
287        for x in 0..w {
288            let stripe_on = stripe_on_at(x, scroll, stripe_w);
289            let mut color = if stripe_on && stripe_strength > 0 {
290                blend_rgb565(base, sky.stripe_color, stripe_strength)
291            } else {
292                base
293            };
294            color = apply_post(color, screen_tint, palette_mode);
295            fb.draw_iter([Pixel(Point::new(x, y), color)])
296                .map_err(|_| RenderError::InvalidInput("draw target rejected sky write"))?;
297        }
298    }
299    Ok(())
300}
301
302#[cfg(test)]
303mod tests {
304    extern crate std;
305    use super::stripe_on_at;
306
307    #[test]
308    fn stripe_phase_is_periodic_with_negative_scroll() {
309        let stripe_w = 10;
310        let scroll = -3;
311        for x in -40..40 {
312            assert_eq!(
313                stripe_on_at(x, scroll, stripe_w),
314                stripe_on_at(x + stripe_w * 2, scroll, stripe_w)
315            );
316        }
317    }
318
319    #[test]
320    fn stripe_runs_do_not_exceed_width() {
321        let stripe_w = 8;
322        let scroll = -5;
323        let mut max_run = 0usize;
324        let mut run = 0usize;
325        let mut prev = stripe_on_at(-64, scroll, stripe_w);
326        for x in -63..=64 {
327            let cur = stripe_on_at(x, scroll, stripe_w);
328            if cur == prev {
329                run += 1;
330            } else {
331                max_run = max_run.max(run);
332                run = 1;
333                prev = cur;
334            }
335        }
336        max_run = max_run.max(run);
337        assert!(max_run <= stripe_w as usize);
338    }
339
340    #[test]
341    fn test_execute_commands_with_picking() {
342        use crate::command_buffer::{CommandBuffer, RenderCommand};
343        use crate::primitive::DrawPrimitive;
344        use embedded_graphics_core::pixelcolor::{Rgb565, RgbColor};
345        use embedded_graphics_framebuf::{
346            FrameBuf,
347            backends::{EndianCorrectedBuffer, EndianCorrection},
348        };
349        use nalgebra::Point2;
350
351        let backing = std::vec![Rgb565::BLACK; 64 * 64].leak();
352        let mut fb = FrameBuf::new(
353            EndianCorrectedBuffer::new(backing, EndianCorrection::ToLittleEndian),
354            64,
355            64,
356        );
357        let mut zbuf = [crate::Z_MAX_VALUE; 64 * 64];
358        let mut frame = super::FrameCtx {
359            zbuffer: &mut zbuf,
360            width: 64,
361            height: 64,
362        };
363
364        let mut cmd = CommandBuffer::<8>::new();
365        cmd.push(RenderCommand::Draw(
366            DrawPrimitive::ColoredTriangleWithDepth {
367                points: [
368                    Point2::new(10, 10),
369                    Point2::new(40, 10),
370                    Point2::new(25, 40),
371                ],
372                depths: [10.0, 10.0, 10.0],
373                color: Rgb565::RED,
374            },
375        ))
376        .unwrap();
377
378        let queries = [super::PickQuery::new(25, 20), super::PickQuery::new(0, 0)];
379        let mut results = [None, None];
380
381        let region =
382            super::execute_commands_with_picking(&mut fb, &mut frame, &cmd, &queries, &mut results)
383                .unwrap();
384
385        assert!(region.is_some());
386        assert!(results[0].is_some());
387        let hit = results[0].unwrap();
388        assert_eq!(hit.x, 25);
389        assert_eq!(hit.y, 20);
390        assert_eq!(hit.command_index, 0);
391        assert!(results[1].is_none()); // (0, 0) was outside triangle
392    }
393
394    #[test]
395    fn test_execute_commands_variants() {
396        use crate::command_buffer::{CommandBuffer, RenderCommand};
397        use crate::primitive::DrawPrimitive;
398        use embedded_graphics_core::pixelcolor::{Rgb565, RgbColor};
399        use embedded_graphics_framebuf::{
400            FrameBuf,
401            backends::{EndianCorrectedBuffer, EndianCorrection},
402        };
403        use nalgebra::Point2;
404
405        let backing = std::vec![Rgb565::BLACK; 16 * 16].leak();
406        let mut fb = FrameBuf::new(
407            EndianCorrectedBuffer::new(backing, EndianCorrection::ToLittleEndian),
408            16,
409            16,
410        );
411        let mut zbuf = [crate::Z_MAX_VALUE; 16 * 16];
412        let mut frame = super::FrameCtx {
413            zbuffer: &mut zbuf,
414            width: 16,
415            height: 16,
416        };
417
418        let mut cmd = CommandBuffer::<4>::new();
419        cmd.push(RenderCommand::ClearColor(Rgb565::BLUE)).unwrap();
420        cmd.push(RenderCommand::ClearDepth(crate::Z_MAX_VALUE))
421            .unwrap();
422        cmd.push(RenderCommand::Draw(
423            DrawPrimitive::ColoredTriangleWithDepth {
424                points: [Point2::new(2, 2), Point2::new(12, 2), Point2::new(7, 12)],
425                depths: [10.0; 3],
426                color: Rgb565::RED,
427            },
428        ))
429        .unwrap();
430
431        assert!(super::execute_commands(&mut fb, &mut frame, &cmd, None).is_ok());
432
433        let dirty =
434            super::execute_commands_with_dirty_region(&mut fb, &mut frame, &cmd, None).unwrap();
435        assert!(dirty.is_some());
436        let dirty = dirty.unwrap();
437        assert!(dirty.width >= 1);
438        assert!(dirty.height >= 1);
439
440        let region = super::execute_commands_with_dirty_region_effects(
441            &mut fb,
442            &mut frame,
443            &cmd,
444            None,
445            None,
446            None,
447            crate::retro::StippleMode::Off,
448            crate::retro::PaletteMode::Off,
449            None,
450            [0.0, 0.0, -1.0],
451        )
452        .unwrap();
453        assert!(region.is_some());
454
455        // Sky path reports the full frame dirty.
456        let full_region = super::execute_commands_with_dirty_region_effects(
457            &mut fb,
458            &mut frame,
459            &cmd,
460            None,
461            None,
462            None,
463            crate::retro::StippleMode::Off,
464            crate::retro::PaletteMode::Off,
465            Some(crate::retro::SkyConfig::retro_blue()),
466            [0.0, 0.0, -1.0],
467        )
468        .unwrap();
469        let full = full_region.unwrap();
470        assert_eq!(full.x, 0);
471        assert_eq!(full.y, 0);
472        assert_eq!(full.width, 16);
473        assert_eq!(full.height, 16);
474    }
475
476    #[test]
477    fn test_execute_commands_tiled_and_frame_validation() {
478        use crate::command_buffer::{CommandBuffer, RenderCommand};
479        use crate::primitive::DrawPrimitive;
480        use embedded_graphics_core::pixelcolor::{Rgb565, RgbColor};
481        use embedded_graphics_framebuf::{
482            FrameBuf,
483            backends::{EndianCorrectedBuffer, EndianCorrection},
484        };
485        use nalgebra::Point2;
486
487        let backing = std::vec![Rgb565::BLACK; 16 * 16].leak();
488        let mut fb = FrameBuf::new(
489            EndianCorrectedBuffer::new(backing, EndianCorrection::ToLittleEndian),
490            16,
491            16,
492        );
493
494        // Validation fails on mismatched zbuffer length.
495        let mut short_zbuf = [crate::Z_MAX_VALUE; 4];
496        let mut bad_frame = super::FrameCtx {
497            zbuffer: &mut short_zbuf,
498            width: 16,
499            height: 16,
500        };
501        let empty = CommandBuffer::<1>::new();
502        assert!(super::execute_commands(&mut fb, &mut bad_frame, &empty, None).is_err());
503
504        let mut zbuf = [crate::Z_MAX_VALUE; 16 * 16];
505        let mut frame = super::FrameCtx {
506            zbuffer: &mut zbuf,
507            width: 16,
508            height: 16,
509        };
510
511        let mut cmd = CommandBuffer::<2>::new();
512        cmd.push(RenderCommand::Draw(
513            DrawPrimitive::ColoredTriangleWithDepth {
514                points: [Point2::new(3, 3), Point2::new(13, 3), Point2::new(8, 13)],
515                depths: [5.0; 3],
516                color: Rgb565::GREEN,
517            },
518        ))
519        .unwrap();
520
521        let stats = super::execute_commands_tiled::<_, 2, 16>(
522            &mut fb,
523            &mut frame,
524            &cmd,
525            crate::tilebin::TileConfig {
526                tile_width: 8,
527                tile_height: 8,
528            },
529            None,
530        )
531        .unwrap();
532        assert!(stats.draw_commands >= 1);
533        assert!(stats.bins_used >= 1);
534    }
535}
536
537pub fn execute_commands<D, const MAX: usize>(
538    fb: &mut D,
539    frame: &mut FrameCtx<'_>,
540    cmd: &CommandBuffer<MAX>,
541    fog: Option<&FogConfig>,
542) -> Result<(), RenderError>
543where
544    D: DrawTarget<Color = Rgb565> + OriginDimensions,
545    D::Error: Debug,
546{
547    let _ = execute_commands_with_dirty_region_effects(
548        fb,
549        frame,
550        cmd,
551        fog,
552        None,
553        None,
554        StippleMode::Off,
555        PaletteMode::Off,
556        None,
557        [0.0, 0.0, -1.0],
558    )?;
559    Ok(())
560}
561
562pub fn execute_commands_with_dirty_region<D, const MAX: usize>(
563    fb: &mut D,
564    frame: &mut FrameCtx<'_>,
565    cmd: &CommandBuffer<MAX>,
566    fog: Option<&FogConfig>,
567) -> Result<Option<DirtyRegion>, RenderError>
568where
569    D: DrawTarget<Color = Rgb565> + OriginDimensions,
570    D::Error: Debug,
571{
572    execute_commands_with_dirty_region_effects(
573        fb,
574        frame,
575        cmd,
576        fog,
577        None,
578        None,
579        StippleMode::Off,
580        PaletteMode::Off,
581        None,
582        [0.0, 0.0, -1.0],
583    )
584}
585
586pub fn execute_commands_with_dirty_region_effects<D, const MAX: usize>(
587    fb: &mut D,
588    frame: &mut FrameCtx<'_>,
589    cmd: &CommandBuffer<MAX>,
590    fog: Option<&FogConfig>,
591    dither: Option<&DitherConfig>,
592    screen_tint: Option<ScreenTint>,
593    _stipple_mode: StippleMode,
594    palette_mode: PaletteMode,
595    sky: Option<SkyConfig>,
596    camera_dir: [f32; 3],
597) -> Result<Option<DirtyRegion>, RenderError>
598where
599    D: DrawTarget<Color = Rgb565> + OriginDimensions,
600    D::Error: Debug,
601{
602    frame.validate()?;
603    let mut dirty_bounds: Option<(i32, i32, i32, i32)> = None;
604    if let Some(sky_cfg) = sky {
605        draw_sky_background(
606            fb,
607            frame.width,
608            frame.height,
609            sky_cfg,
610            camera_dir,
611            screen_tint,
612            palette_mode,
613        )?;
614        dirty_bounds = Some((
615            0,
616            0,
617            frame.width.saturating_sub(1) as i32,
618            frame.height.saturating_sub(1) as i32,
619        ));
620    }
621
622    for c in cmd.iter() {
623        match c {
624            RenderCommand::ClearColor(color) => {
625                let w = frame.width as i32;
626                let h = frame.height as i32;
627                let clear_color = apply_post(*color, screen_tint, palette_mode);
628                for y in 0..h {
629                    for x in 0..w {
630                        fb.draw_iter([Pixel(Point::new(x, y), clear_color)])
631                            .map_err(|_| {
632                                RenderError::InvalidInput("draw target rejected clear write")
633                            })?;
634                    }
635                }
636            }
637            RenderCommand::ClearDepth(value) => {
638                crate::clear_zbuffer(frame.zbuffer, *value);
639            }
640            RenderCommand::Draw(primitive) => {
641                let prim = tint_primitive(primitive, screen_tint, palette_mode);
642                draw_zbuffered_with_effects(prim, fb, frame.zbuffer, frame.width, fog, dither);
643                let (min_x, min_y, max_x, max_y) = primitive_bounds(primitive);
644                if let Some((min_x, min_y, max_x, max_y)) =
645                    clamp_bounds_to_frame(min_x, min_y, max_x, max_y, frame.width, frame.height)
646                {
647                    dirty_bounds = Some(match dirty_bounds {
648                        Some((cx0, cy0, cx1, cy1)) => (
649                            cx0.min(min_x),
650                            cy0.min(min_y),
651                            cx1.max(max_x),
652                            cy1.max(max_y),
653                        ),
654                        None => (min_x, min_y, max_x, max_y),
655                    });
656                }
657            }
658        }
659    }
660
661    let region = dirty_bounds.and_then(|(x0, y0, x1, y1)| DirtyRegion::from_bounds(x0, y0, x1, y1));
662    Ok(region)
663}
664
665/// Like [`execute_commands_with_dirty_region_effects`], but resolves
666/// [`DrawPrimitive::TexturedTriangleWithDepth`]/[`DrawPrimitive::LightmappedTriangle`]
667/// via `texture_manager` instead of silently dropping them (mirrors
668/// `bsp::execute_bsp_textured`'s dispatch pattern). Non-textured primitives
669/// still go through `tint_primitive` + `draw_zbuffered_with_effects`, same
670/// as the non-textured `execute*` functions, so mixing textured and
671/// flat-colored meshes in one scene keeps consistent tint/palette
672/// behavior.
673#[allow(clippy::too_many_arguments)]
674#[cfg(feature = "textured")]
675pub fn execute_commands_with_dirty_region_effects_textured<D, const MAX: usize, const N: usize>(
676    fb: &mut D,
677    frame: &mut FrameCtx<'_>,
678    cmd: &CommandBuffer<MAX>,
679    texture_manager: &crate::texture::TextureManager<N>,
680    fog: Option<&FogConfig>,
681    dither: Option<&DitherConfig>,
682    screen_tint: Option<ScreenTint>,
683    stipple_mode: StippleMode,
684    palette_mode: PaletteMode,
685    sky: Option<SkyConfig>,
686    camera_dir: [f32; 3],
687) -> Result<Option<DirtyRegion>, RenderError>
688where
689    D: DrawTarget<Color = Rgb565> + OriginDimensions,
690    D::Error: Debug,
691{
692    use crate::draw::{draw_zbuffered_lightmapped_mapped, draw_zbuffered_with_textures_mapped};
693    use crate::retro::TextureMapping;
694
695    frame.validate()?;
696    let mut dirty_bounds: Option<(i32, i32, i32, i32)> = None;
697    if let Some(sky_cfg) = sky {
698        draw_sky_background(
699            fb,
700            frame.width,
701            frame.height,
702            sky_cfg,
703            camera_dir,
704            screen_tint,
705            palette_mode,
706        )?;
707        dirty_bounds = Some((
708            0,
709            0,
710            frame.width.saturating_sub(1) as i32,
711            frame.height.saturating_sub(1) as i32,
712        ));
713    }
714
715    for c in cmd.iter() {
716        match c {
717            RenderCommand::ClearColor(color) => {
718                let w = frame.width as i32;
719                let h = frame.height as i32;
720                let clear_color = apply_post(*color, screen_tint, palette_mode);
721                for y in 0..h {
722                    for x in 0..w {
723                        fb.draw_iter([Pixel(Point::new(x, y), clear_color)])
724                            .map_err(|_| {
725                                RenderError::InvalidInput("draw target rejected clear write")
726                            })?;
727                    }
728                }
729            }
730            RenderCommand::ClearDepth(value) => {
731                crate::clear_zbuffer(frame.zbuffer, *value);
732            }
733            RenderCommand::Draw(primitive) => {
734                match primitive {
735                    #[cfg(feature = "textured")]
736                    DrawPrimitive::LightmappedTriangle {
737                        points,
738                        depths,
739                        ws,
740                        surface_uvs,
741                        lm_uvs,
742                        texture_id,
743                        lightmap_id,
744                        brightness,
745                        dynamic_tint,
746                    } => {
747                        draw_zbuffered_lightmapped_mapped(
748                            *points,
749                            *depths,
750                            *ws,
751                            *surface_uvs,
752                            *lm_uvs,
753                            *texture_id,
754                            *lightmap_id,
755                            *brightness,
756                            *dynamic_tint,
757                            fog,
758                            texture_manager,
759                            fb,
760                            frame.zbuffer,
761                            frame.width,
762                            TextureMapping::PerspectiveCorrect,
763                            stipple_mode,
764                            screen_tint,
765                            palette_mode,
766                        );
767                    }
768                    #[cfg(feature = "textured")]
769                    DrawPrimitive::TexturedTriangle { .. }
770                    | DrawPrimitive::TexturedTriangleWithDepth { .. }
771                    | DrawPrimitive::TexturedGouraudTriangleWithDepth { .. } => {
772                        draw_zbuffered_with_textures_mapped(
773                            primitive.clone(),
774                            fb,
775                            frame.zbuffer,
776                            frame.width,
777                            texture_manager,
778                            fog,
779                            dither,
780                            TextureMapping::PerspectiveCorrect,
781                            stipple_mode,
782                            screen_tint,
783                            palette_mode,
784                        );
785                    }
786                    _ => {
787                        let prim = tint_primitive(primitive, screen_tint, palette_mode);
788                        draw_zbuffered_with_effects(
789                            prim,
790                            fb,
791                            frame.zbuffer,
792                            frame.width,
793                            fog,
794                            dither,
795                        );
796                    }
797                }
798                let (min_x, min_y, max_x, max_y) = primitive_bounds(primitive);
799                if let Some((min_x, min_y, max_x, max_y)) =
800                    clamp_bounds_to_frame(min_x, min_y, max_x, max_y, frame.width, frame.height)
801                {
802                    dirty_bounds = Some(match dirty_bounds {
803                        Some((cx0, cy0, cx1, cy1)) => (
804                            cx0.min(min_x),
805                            cy0.min(min_y),
806                            cx1.max(max_x),
807                            cy1.max(max_y),
808                        ),
809                        None => (min_x, min_y, max_x, max_y),
810                    });
811                }
812            }
813        }
814    }
815
816    let region = dirty_bounds.and_then(|(x0, y0, x1, y1)| DirtyRegion::from_bounds(x0, y0, x1, y1));
817    Ok(region)
818}
819
820pub fn execute_commands_tiled<D, const MAX: usize, const BIN_CAP: usize>(
821    fb: &mut D,
822    frame: &mut FrameCtx<'_>,
823    cmd: &CommandBuffer<MAX>,
824    tile: crate::tilebin::TileConfig,
825    fog: Option<&FogConfig>,
826) -> Result<crate::tilebin::TileBinStats, RenderError>
827where
828    D: DrawTarget<Color = Rgb565> + OriginDimensions,
829    D::Error: Debug,
830{
831    execute_commands_tiled_effects::<D, MAX, BIN_CAP>(
832        fb,
833        frame,
834        cmd,
835        tile,
836        fog,
837        None,
838        None,
839        StippleMode::Off,
840        PaletteMode::Off,
841        None,
842        [0.0, 0.0, -1.0],
843    )
844}
845
846pub fn execute_commands_tiled_effects<D, const MAX: usize, const BIN_CAP: usize>(
847    fb: &mut D,
848    frame: &mut FrameCtx<'_>,
849    cmd: &CommandBuffer<MAX>,
850    tile: crate::tilebin::TileConfig,
851    fog: Option<&FogConfig>,
852    dither: Option<&DitherConfig>,
853    screen_tint: Option<ScreenTint>,
854    _stipple_mode: StippleMode,
855    palette_mode: PaletteMode,
856    sky: Option<SkyConfig>,
857    camera_dir: [f32; 3],
858) -> Result<crate::tilebin::TileBinStats, RenderError>
859where
860    D: DrawTarget<Color = Rgb565> + OriginDimensions,
861    D::Error: Debug,
862{
863    frame.validate()?;
864    if let Some(sky_cfg) = sky {
865        draw_sky_background(
866            fb,
867            frame.width,
868            frame.height,
869            sky_cfg,
870            camera_dir,
871            screen_tint,
872            palette_mode,
873        )?;
874    }
875    let (bins, stats) =
876        crate::tilebin::build_bins::<MAX, BIN_CAP>(cmd, frame.width, frame.height, tile)?;
877    let mut executed_draw = [false; MAX];
878
879    for command in cmd.iter() {
880        match command {
881            RenderCommand::ClearColor(color) => {
882                let w = frame.width as i32;
883                let h = frame.height as i32;
884                let clear_color = apply_post(*color, screen_tint, palette_mode);
885                for y in 0..h {
886                    for x in 0..w {
887                        fb.draw_iter([Pixel(Point::new(x, y), clear_color)])
888                            .map_err(|_| {
889                                RenderError::InvalidInput("draw target rejected clear write")
890                            })?;
891                    }
892                }
893            }
894            RenderCommand::ClearDepth(value) => crate::clear_zbuffer(frame.zbuffer, *value),
895            RenderCommand::Draw(_) => {}
896        }
897    }
898
899    for bin in bins.iter() {
900        for idx in bin.iter().copied() {
901            if idx >= MAX || executed_draw[idx] {
902                continue;
903            }
904            let Some(RenderCommand::Draw(primitive)) = cmd.get(idx) else {
905                continue;
906            };
907            let prim = tint_primitive(primitive, screen_tint, palette_mode);
908            draw_zbuffered_with_effects(prim, fb, frame.zbuffer, frame.width, fog, dither);
909            executed_draw[idx] = true;
910        }
911    }
912
913    Ok(stats)
914}
915
916/// Execute commands using 2xSSAA (Super-Sampling Anti-Aliasing) scanline rasterization.
917#[cfg(feature = "aa")]
918pub fn execute_commands_2xssaa<D, const MAX: usize>(
919    fb: &mut D,
920    frame: &mut FrameCtx<'_>,
921    cmd_buf: &CommandBuffer<MAX>,
922) -> Result<(), RenderError>
923where
924    D: DrawTarget<Color = Rgb565> + crate::draw::ReadPixel,
925    <D as DrawTarget>::Error: Debug,
926{
927    frame.validate()?;
928    for c in cmd_buf.iter() {
929        if let RenderCommand::Draw(primitive) = c {
930            crate::draw::draw_zbuffered_2xssaa(primitive.clone(), fb, frame.zbuffer, frame.width);
931        }
932    }
933    Ok(())
934}
935
936/// Execute commands and evaluate integrated screen-space pick queries during the rasterization pass.
937pub fn execute_commands_with_picking<D, const MAX: usize>(
938    fb: &mut D,
939    frame: &mut FrameCtx<'_>,
940    cmd: &CommandBuffer<MAX>,
941    queries: &[PickQuery],
942    results: &mut [Option<PickResult>],
943) -> Result<Option<DirtyRegion>, RenderError>
944where
945    D: DrawTarget<Color = Rgb565>,
946    <D as DrawTarget>::Error: Debug,
947{
948    frame.validate()?;
949    let mut dirty_bounds: Option<(i32, i32, i32, i32)> = None;
950
951    for (cmd_idx, c) in cmd.iter().enumerate() {
952        match c {
953            RenderCommand::ClearColor(color) => {
954                let w = frame.width as i32;
955                let h = frame.height as i32;
956                for y in 0..h {
957                    for x in 0..w {
958                        fb.draw_iter([Pixel(Point::new(x, y), *color)])
959                            .map_err(|_| {
960                                RenderError::InvalidInput("draw target rejected clear write")
961                            })?;
962                    }
963                }
964            }
965            RenderCommand::ClearDepth(value) => {
966                crate::clear_zbuffer(frame.zbuffer, *value);
967            }
968            RenderCommand::Draw(primitive) => {
969                let mut prev_depths = [0 as crate::ZDepth; 16];
970                let check_count = queries.len().min(16).min(results.len());
971                for (q_i, query) in queries.iter().take(check_count).enumerate() {
972                    if query.x >= 0
973                        && query.x < frame.width as i32
974                        && query.y >= 0
975                        && query.y < frame.height as i32
976                    {
977                        let idx = query.y as usize * frame.width + query.x as usize;
978                        prev_depths[q_i] = frame.zbuffer[idx];
979                    }
980                }
981
982                crate::draw::draw_zbuffered(primitive.clone(), fb, frame.zbuffer, frame.width);
983
984                for (q_i, query) in queries.iter().take(check_count).enumerate() {
985                    if query.x >= 0
986                        && query.x < frame.width as i32
987                        && query.y >= 0
988                        && query.y < frame.height as i32
989                    {
990                        let idx = query.y as usize * frame.width + query.x as usize;
991                        let new_depth = frame.zbuffer[idx];
992                        if new_depth < prev_depths[q_i] {
993                            results[q_i] = Some(PickResult {
994                                x: query.x,
995                                y: query.y,
996                                depth: new_depth,
997                                command_index: cmd_idx,
998                            });
999                        }
1000                    }
1001                }
1002
1003                let (min_x, min_y, max_x, max_y) = primitive_bounds(primitive);
1004                if let Some((min_x, min_y, max_x, max_y)) =
1005                    clamp_bounds_to_frame(min_x, min_y, max_x, max_y, frame.width, frame.height)
1006                {
1007                    dirty_bounds = Some(match dirty_bounds {
1008                        Some((cx0, cy0, cx1, cy1)) => (
1009                            cx0.min(min_x),
1010                            cy0.min(min_y),
1011                            cx1.max(max_x),
1012                            cy1.max(max_y),
1013                        ),
1014                        None => (min_x, min_y, max_x, max_y),
1015                    });
1016                }
1017            }
1018        }
1019    }
1020
1021    let region = dirty_bounds.and_then(|(x0, y0, x1, y1)| DirtyRegion::from_bounds(x0, y0, x1, y1));
1022    Ok(region)
1023}