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    DrawPrimitive,
12    command_buffer::{CommandBuffer, RenderCommand},
13    draw::{DitherConfig, FogConfig, draw_zbuffered_with_effects},
14    error::{BudgetKind, RenderError},
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
59fn primitive_bounds(primitive: &DrawPrimitive) -> (i32, i32, i32, i32) {
60    match primitive {
61        DrawPrimitive::ColoredPoint(p, _) => (p.x, p.y, p.x, p.y),
62        DrawPrimitive::Line([a, b], _) => (a.x.min(b.x), a.y.min(b.y), a.x.max(b.x), a.y.max(b.y)),
63        DrawPrimitive::ColoredTriangle(points, _)
64        | DrawPrimitive::ColoredTriangleWithDepth { points, .. }
65        | DrawPrimitive::TranslucentTriangleWithDepth { points, .. }
66        | DrawPrimitive::GouraudTriangle { points, .. }
67        | DrawPrimitive::GouraudTriangleWithDepth { points, .. }
68        | DrawPrimitive::TexturedTriangle { points, .. }
69        | DrawPrimitive::TexturedTriangleWithDepth { points, .. }
70        | DrawPrimitive::TexturedGouraudTriangleWithDepth { points, .. }
71        | DrawPrimitive::LightmappedTriangle { points, .. } => {
72            let min_x = points.iter().map(|p| p.x).min().unwrap_or(0);
73            let min_y = points.iter().map(|p| p.y).min().unwrap_or(0);
74            let max_x = points.iter().map(|p| p.x).max().unwrap_or(0);
75            let max_y = points.iter().map(|p| p.y).max().unwrap_or(0);
76            (min_x, min_y, max_x, max_y)
77        }
78    }
79}
80
81fn clamp_bounds_to_frame(
82    min_x: i32,
83    min_y: i32,
84    max_x: i32,
85    max_y: i32,
86    width: usize,
87    height: usize,
88) -> Option<(i32, i32, i32, i32)> {
89    let w = width as i32;
90    let h = height as i32;
91    let clamped_min_x = min_x.clamp(0, w.saturating_sub(1));
92    let clamped_min_y = min_y.clamp(0, h.saturating_sub(1));
93    let clamped_max_x = max_x.clamp(0, w.saturating_sub(1));
94    let clamped_max_y = max_y.clamp(0, h.saturating_sub(1));
95    if clamped_max_x < clamped_min_x || clamped_max_y < clamped_min_y {
96        return None;
97    }
98    Some((clamped_min_x, clamped_min_y, clamped_max_x, clamped_max_y))
99}
100
101#[inline]
102fn apply_post(color: Rgb565, tint: Option<ScreenTint>, palette_mode: PaletteMode) -> Rgb565 {
103    let tinted = if let Some(t) = tint {
104        t.apply(color)
105    } else {
106        color
107    };
108    palette_mode.apply(tinted)
109}
110
111fn tint_primitive(
112    primitive: &DrawPrimitive,
113    tint: Option<ScreenTint>,
114    palette_mode: PaletteMode,
115) -> DrawPrimitive {
116    match primitive.clone() {
117        DrawPrimitive::ColoredPoint(p, color) => {
118            DrawPrimitive::ColoredPoint(p, apply_post(color, tint, palette_mode))
119        }
120        DrawPrimitive::Line(points, color) => {
121            DrawPrimitive::Line(points, apply_post(color, tint, palette_mode))
122        }
123        DrawPrimitive::ColoredTriangle(points, color) => {
124            DrawPrimitive::ColoredTriangle(points, apply_post(color, tint, palette_mode))
125        }
126        DrawPrimitive::ColoredTriangleWithDepth {
127            points,
128            depths,
129            color,
130        } => DrawPrimitive::ColoredTriangleWithDepth {
131            points,
132            depths,
133            color: apply_post(color, tint, palette_mode),
134        },
135        DrawPrimitive::TranslucentTriangleWithDepth {
136            points,
137            depths,
138            color,
139            alpha,
140        } => DrawPrimitive::TranslucentTriangleWithDepth {
141            points,
142            depths,
143            color: apply_post(color, tint, palette_mode),
144            alpha,
145        },
146        DrawPrimitive::GouraudTriangle { points, colors } => DrawPrimitive::GouraudTriangle {
147            points,
148            colors: [
149                apply_post(colors[0], tint, palette_mode),
150                apply_post(colors[1], tint, palette_mode),
151                apply_post(colors[2], tint, palette_mode),
152            ],
153        },
154        DrawPrimitive::GouraudTriangleWithDepth {
155            points,
156            depths,
157            colors,
158        } => DrawPrimitive::GouraudTriangleWithDepth {
159            points,
160            depths,
161            colors: [
162                apply_post(colors[0], tint, palette_mode),
163                apply_post(colors[1], tint, palette_mode),
164                apply_post(colors[2], tint, palette_mode),
165            ],
166        },
167        DrawPrimitive::LightmappedTriangle {
168            points,
169            depths,
170            ws,
171            surface_uvs,
172            lm_uvs,
173            texture_id,
174            lightmap_id,
175            brightness,
176            dynamic_tint,
177        } => DrawPrimitive::LightmappedTriangle {
178            points,
179            depths,
180            ws,
181            surface_uvs,
182            lm_uvs,
183            texture_id,
184            lightmap_id,
185            brightness,
186            dynamic_tint: apply_post(dynamic_tint, tint, palette_mode),
187        },
188        DrawPrimitive::TexturedGouraudTriangleWithDepth {
189            points,
190            depths,
191            ws,
192            uvs,
193            colors,
194            texture_id,
195        } => DrawPrimitive::TexturedGouraudTriangleWithDepth {
196            points,
197            depths,
198            ws,
199            uvs,
200            colors: [
201                apply_post(colors[0], tint, palette_mode),
202                apply_post(colors[1], tint, palette_mode),
203                apply_post(colors[2], tint, palette_mode),
204            ],
205            texture_id,
206        },
207        other => other,
208    }
209}
210
211#[inline]
212fn blend_rgb565(a: Rgb565, b: Rgb565, t_q8: u16) -> Rgb565 {
213    let inv = 255u16.saturating_sub(t_q8);
214    let r = ((a.r() as u16 * inv + b.r() as u16 * t_q8) / 255) as u8;
215    let g = ((a.g() as u16 * inv + b.g() as u16 * t_q8) / 255) as u8;
216    let bch = ((a.b() as u16 * inv + b.b() as u16 * t_q8) / 255) as u8;
217    Rgb565::new(r, g, bch)
218}
219
220#[inline]
221fn stripe_on_at(x: i32, scroll: i32, stripe_w: i32) -> bool {
222    (((x + scroll).div_euclid(stripe_w)) & 1) == 0
223}
224
225fn draw_sky_background<D>(
226    fb: &mut D,
227    width: usize,
228    height: usize,
229    sky: SkyConfig,
230    camera_dir: [f32; 3],
231    screen_tint: Option<ScreenTint>,
232    palette_mode: PaletteMode,
233) -> Result<(), RenderError>
234where
235    D: DrawTarget<Color = Rgb565> + OriginDimensions,
236    D::Error: Debug,
237{
238    let w = width as i32;
239    let h = height as i32;
240    if w <= 0 || h <= 0 {
241        return Ok(());
242    }
243
244    let horizon = (h as f32 * (0.5 + camera_dir[1].clamp(-1.0, 1.0) * 0.25)) as i32;
245    let stripe_w = sky.stripe_width.max(1) as i32;
246    let scroll = (camera_dir[0] * 128.0) as i32;
247    let stripe_fade_span = (h / 6).max(1);
248
249    for y in 0..h {
250        let dy = (y - horizon + h / 2).clamp(0, h);
251        let t_q8 = ((dy as i64 * 255) / h.max(1) as i64) as u16;
252        let base = blend_rgb565(sky.top_color, sky.bottom_color, t_q8);
253        let below_horizon = (y - horizon).max(0);
254        let stripe_strength = if below_horizon == 0 {
255            sky.stripe_strength as u16
256        } else if below_horizon >= stripe_fade_span {
257            0
258        } else {
259            let rem = stripe_fade_span - below_horizon;
260            ((sky.stripe_strength as i32 * rem) / stripe_fade_span) as u16
261        };
262        for x in 0..w {
263            let stripe_on = stripe_on_at(x, scroll, stripe_w);
264            let mut color = if stripe_on && stripe_strength > 0 {
265                blend_rgb565(base, sky.stripe_color, stripe_strength)
266            } else {
267                base
268            };
269            color = apply_post(color, screen_tint, palette_mode);
270            fb.draw_iter([Pixel(Point::new(x, y), color)])
271                .map_err(|_| RenderError::InvalidInput("draw target rejected sky write"))?;
272        }
273    }
274    Ok(())
275}
276
277#[cfg(test)]
278mod tests {
279    use super::stripe_on_at;
280
281    #[test]
282    fn stripe_phase_is_periodic_with_negative_scroll() {
283        let stripe_w = 10;
284        let scroll = -3;
285        for x in -40..40 {
286            assert_eq!(
287                stripe_on_at(x, scroll, stripe_w),
288                stripe_on_at(x + stripe_w * 2, scroll, stripe_w)
289            );
290        }
291    }
292
293    #[test]
294    fn stripe_runs_do_not_exceed_width() {
295        let stripe_w = 8;
296        let scroll = -5;
297        let mut max_run = 0usize;
298        let mut run = 0usize;
299        let mut prev = stripe_on_at(-64, scroll, stripe_w);
300        for x in -63..=64 {
301            let cur = stripe_on_at(x, scroll, stripe_w);
302            if cur == prev {
303                run += 1;
304            } else {
305                max_run = max_run.max(run);
306                run = 1;
307                prev = cur;
308            }
309        }
310        max_run = max_run.max(run);
311        assert!(max_run <= stripe_w as usize);
312    }
313}
314
315pub fn execute_commands<D, const MAX: usize>(
316    fb: &mut D,
317    frame: &mut FrameCtx<'_>,
318    cmd: &CommandBuffer<MAX>,
319    fog: Option<&FogConfig>,
320) -> Result<(), RenderError>
321where
322    D: DrawTarget<Color = Rgb565> + OriginDimensions,
323    D::Error: Debug,
324{
325    let _ = execute_commands_with_dirty_region_effects(
326        fb,
327        frame,
328        cmd,
329        fog,
330        None,
331        None,
332        StippleMode::Off,
333        PaletteMode::Off,
334        None,
335        [0.0, 0.0, -1.0],
336    )?;
337    Ok(())
338}
339
340pub fn execute_commands_with_dirty_region<D, const MAX: usize>(
341    fb: &mut D,
342    frame: &mut FrameCtx<'_>,
343    cmd: &CommandBuffer<MAX>,
344    fog: Option<&FogConfig>,
345) -> Result<Option<DirtyRegion>, RenderError>
346where
347    D: DrawTarget<Color = Rgb565> + OriginDimensions,
348    D::Error: Debug,
349{
350    execute_commands_with_dirty_region_effects(
351        fb,
352        frame,
353        cmd,
354        fog,
355        None,
356        None,
357        StippleMode::Off,
358        PaletteMode::Off,
359        None,
360        [0.0, 0.0, -1.0],
361    )
362}
363
364pub fn execute_commands_with_dirty_region_effects<D, const MAX: usize>(
365    fb: &mut D,
366    frame: &mut FrameCtx<'_>,
367    cmd: &CommandBuffer<MAX>,
368    fog: Option<&FogConfig>,
369    dither: Option<&DitherConfig>,
370    screen_tint: Option<ScreenTint>,
371    _stipple_mode: StippleMode,
372    palette_mode: PaletteMode,
373    sky: Option<SkyConfig>,
374    camera_dir: [f32; 3],
375) -> Result<Option<DirtyRegion>, RenderError>
376where
377    D: DrawTarget<Color = Rgb565> + OriginDimensions,
378    D::Error: Debug,
379{
380    frame.validate()?;
381    let mut dirty_bounds: Option<(i32, i32, i32, i32)> = None;
382    if let Some(sky_cfg) = sky {
383        draw_sky_background(
384            fb,
385            frame.width,
386            frame.height,
387            sky_cfg,
388            camera_dir,
389            screen_tint,
390            palette_mode,
391        )?;
392        dirty_bounds = Some((
393            0,
394            0,
395            frame.width.saturating_sub(1) as i32,
396            frame.height.saturating_sub(1) as i32,
397        ));
398    }
399
400    for c in cmd.iter() {
401        match c {
402            RenderCommand::ClearColor(color) => {
403                let w = frame.width as i32;
404                let h = frame.height as i32;
405                let clear_color = apply_post(*color, screen_tint, palette_mode);
406                for y in 0..h {
407                    for x in 0..w {
408                        fb.draw_iter([Pixel(Point::new(x, y), clear_color)])
409                            .map_err(|_| {
410                                RenderError::InvalidInput("draw target rejected clear write")
411                            })?;
412                    }
413                }
414            }
415            RenderCommand::ClearDepth(value) => {
416                crate::clear_zbuffer(frame.zbuffer, *value);
417            }
418            RenderCommand::Draw(primitive) => {
419                let prim = tint_primitive(primitive, screen_tint, palette_mode);
420                draw_zbuffered_with_effects(prim, fb, frame.zbuffer, frame.width, fog, dither);
421                let (min_x, min_y, max_x, max_y) = primitive_bounds(primitive);
422                if let Some((min_x, min_y, max_x, max_y)) =
423                    clamp_bounds_to_frame(min_x, min_y, max_x, max_y, frame.width, frame.height)
424                {
425                    dirty_bounds = Some(match dirty_bounds {
426                        Some((cx0, cy0, cx1, cy1)) => (
427                            cx0.min(min_x),
428                            cy0.min(min_y),
429                            cx1.max(max_x),
430                            cy1.max(max_y),
431                        ),
432                        None => (min_x, min_y, max_x, max_y),
433                    });
434                }
435            }
436        }
437    }
438
439    let region = dirty_bounds.and_then(|(x0, y0, x1, y1)| DirtyRegion::from_bounds(x0, y0, x1, y1));
440    Ok(region)
441}
442
443/// Like [`execute_commands_with_dirty_region_effects`], but resolves
444/// [`DrawPrimitive::TexturedTriangleWithDepth`]/[`DrawPrimitive::LightmappedTriangle`]
445/// via `texture_manager` instead of silently dropping them (mirrors
446/// `bsp::execute_bsp_textured`'s dispatch pattern). Non-textured primitives
447/// still go through `tint_primitive` + `draw_zbuffered_with_effects`, same
448/// as the non-textured `execute*` functions, so mixing textured and
449/// flat-colored meshes in one scene keeps consistent tint/palette
450/// behavior.
451#[allow(clippy::too_many_arguments)]
452pub fn execute_commands_with_dirty_region_effects_textured<D, const MAX: usize, const N: usize>(
453    fb: &mut D,
454    frame: &mut FrameCtx<'_>,
455    cmd: &CommandBuffer<MAX>,
456    texture_manager: &crate::texture::TextureManager<N>,
457    fog: Option<&FogConfig>,
458    dither: Option<&DitherConfig>,
459    screen_tint: Option<ScreenTint>,
460    stipple_mode: StippleMode,
461    palette_mode: PaletteMode,
462    sky: Option<SkyConfig>,
463    camera_dir: [f32; 3],
464) -> Result<Option<DirtyRegion>, RenderError>
465where
466    D: DrawTarget<Color = Rgb565> + OriginDimensions,
467    D::Error: Debug,
468{
469    use crate::draw::{draw_zbuffered_lightmapped_mapped, draw_zbuffered_with_textures_mapped};
470    use crate::retro::TextureMapping;
471
472    frame.validate()?;
473    let mut dirty_bounds: Option<(i32, i32, i32, i32)> = None;
474    if let Some(sky_cfg) = sky {
475        draw_sky_background(
476            fb,
477            frame.width,
478            frame.height,
479            sky_cfg,
480            camera_dir,
481            screen_tint,
482            palette_mode,
483        )?;
484        dirty_bounds = Some((
485            0,
486            0,
487            frame.width.saturating_sub(1) as i32,
488            frame.height.saturating_sub(1) as i32,
489        ));
490    }
491
492    for c in cmd.iter() {
493        match c {
494            RenderCommand::ClearColor(color) => {
495                let w = frame.width as i32;
496                let h = frame.height as i32;
497                let clear_color = apply_post(*color, screen_tint, palette_mode);
498                for y in 0..h {
499                    for x in 0..w {
500                        fb.draw_iter([Pixel(Point::new(x, y), clear_color)])
501                            .map_err(|_| {
502                                RenderError::InvalidInput("draw target rejected clear write")
503                            })?;
504                    }
505                }
506            }
507            RenderCommand::ClearDepth(value) => {
508                crate::clear_zbuffer(frame.zbuffer, *value);
509            }
510            RenderCommand::Draw(primitive) => {
511                match primitive {
512                    DrawPrimitive::LightmappedTriangle {
513                        points,
514                        depths,
515                        ws,
516                        surface_uvs,
517                        lm_uvs,
518                        texture_id,
519                        lightmap_id,
520                        brightness,
521                        dynamic_tint,
522                    } => {
523                        draw_zbuffered_lightmapped_mapped(
524                            *points,
525                            *depths,
526                            *ws,
527                            *surface_uvs,
528                            *lm_uvs,
529                            *texture_id,
530                            *lightmap_id,
531                            *brightness,
532                            *dynamic_tint,
533                            fog,
534                            texture_manager,
535                            fb,
536                            frame.zbuffer,
537                            frame.width,
538                            TextureMapping::PerspectiveCorrect,
539                            stipple_mode,
540                            screen_tint,
541                            palette_mode,
542                        );
543                    }
544                    DrawPrimitive::TexturedTriangle { .. }
545                    | DrawPrimitive::TexturedTriangleWithDepth { .. }
546                    | DrawPrimitive::TexturedGouraudTriangleWithDepth { .. } => {
547                        draw_zbuffered_with_textures_mapped(
548                            primitive.clone(),
549                            fb,
550                            frame.zbuffer,
551                            frame.width,
552                            texture_manager,
553                            fog,
554                            dither,
555                            TextureMapping::PerspectiveCorrect,
556                            stipple_mode,
557                            screen_tint,
558                            palette_mode,
559                        );
560                    }
561                    _ => {
562                        let prim = tint_primitive(primitive, screen_tint, palette_mode);
563                        draw_zbuffered_with_effects(
564                            prim,
565                            fb,
566                            frame.zbuffer,
567                            frame.width,
568                            fog,
569                            dither,
570                        );
571                    }
572                }
573                let (min_x, min_y, max_x, max_y) = primitive_bounds(primitive);
574                if let Some((min_x, min_y, max_x, max_y)) =
575                    clamp_bounds_to_frame(min_x, min_y, max_x, max_y, frame.width, frame.height)
576                {
577                    dirty_bounds = Some(match dirty_bounds {
578                        Some((cx0, cy0, cx1, cy1)) => (
579                            cx0.min(min_x),
580                            cy0.min(min_y),
581                            cx1.max(max_x),
582                            cy1.max(max_y),
583                        ),
584                        None => (min_x, min_y, max_x, max_y),
585                    });
586                }
587            }
588        }
589    }
590
591    let region = dirty_bounds.and_then(|(x0, y0, x1, y1)| DirtyRegion::from_bounds(x0, y0, x1, y1));
592    Ok(region)
593}
594
595pub fn execute_commands_tiled<D, const MAX: usize, const BIN_CAP: usize>(
596    fb: &mut D,
597    frame: &mut FrameCtx<'_>,
598    cmd: &CommandBuffer<MAX>,
599    tile: crate::tilebin::TileConfig,
600    fog: Option<&FogConfig>,
601) -> Result<crate::tilebin::TileBinStats, RenderError>
602where
603    D: DrawTarget<Color = Rgb565> + OriginDimensions,
604    D::Error: Debug,
605{
606    execute_commands_tiled_effects::<D, MAX, BIN_CAP>(
607        fb,
608        frame,
609        cmd,
610        tile,
611        fog,
612        None,
613        None,
614        StippleMode::Off,
615        PaletteMode::Off,
616        None,
617        [0.0, 0.0, -1.0],
618    )
619}
620
621pub fn execute_commands_tiled_effects<D, const MAX: usize, const BIN_CAP: usize>(
622    fb: &mut D,
623    frame: &mut FrameCtx<'_>,
624    cmd: &CommandBuffer<MAX>,
625    tile: crate::tilebin::TileConfig,
626    fog: Option<&FogConfig>,
627    dither: Option<&DitherConfig>,
628    screen_tint: Option<ScreenTint>,
629    _stipple_mode: StippleMode,
630    palette_mode: PaletteMode,
631    sky: Option<SkyConfig>,
632    camera_dir: [f32; 3],
633) -> Result<crate::tilebin::TileBinStats, RenderError>
634where
635    D: DrawTarget<Color = Rgb565> + OriginDimensions,
636    D::Error: Debug,
637{
638    frame.validate()?;
639    if let Some(sky_cfg) = sky {
640        draw_sky_background(
641            fb,
642            frame.width,
643            frame.height,
644            sky_cfg,
645            camera_dir,
646            screen_tint,
647            palette_mode,
648        )?;
649    }
650    let (bins, stats) =
651        crate::tilebin::build_bins::<MAX, BIN_CAP>(cmd, frame.width, frame.height, tile)?;
652    let mut executed_draw = [false; MAX];
653
654    for command in cmd.iter() {
655        match command {
656            RenderCommand::ClearColor(color) => {
657                let w = frame.width as i32;
658                let h = frame.height as i32;
659                let clear_color = apply_post(*color, screen_tint, palette_mode);
660                for y in 0..h {
661                    for x in 0..w {
662                        fb.draw_iter([Pixel(Point::new(x, y), clear_color)])
663                            .map_err(|_| {
664                                RenderError::InvalidInput("draw target rejected clear write")
665                            })?;
666                    }
667                }
668            }
669            RenderCommand::ClearDepth(value) => crate::clear_zbuffer(frame.zbuffer, *value),
670            RenderCommand::Draw(_) => {}
671        }
672    }
673
674    for bin in bins.iter() {
675        for idx in bin.iter().copied() {
676            if idx >= MAX || executed_draw[idx] {
677                continue;
678            }
679            let Some(RenderCommand::Draw(primitive)) = cmd.get(idx) else {
680                continue;
681            };
682            let prim = tint_primitive(primitive, screen_tint, palette_mode);
683            draw_zbuffered_with_effects(prim, fb, frame.zbuffer, frame.width, fog, dither);
684            executed_draw[idx] = true;
685        }
686    }
687
688    Ok(stats)
689}
690
691/// Execute commands using 2xSSAA (Super-Sampling Anti-Aliasing) scanline rasterization.
692#[cfg(feature = "aa")]
693pub fn execute_commands_2xssaa<D, const MAX: usize>(
694    fb: &mut D,
695    frame: &mut FrameCtx<'_>,
696    cmd_buf: &CommandBuffer<MAX>,
697) -> Result<(), RenderError>
698where
699    D: DrawTarget<Color = Rgb565> + crate::draw::ReadPixel,
700    <D as DrawTarget>::Error: Debug,
701{
702    frame.validate()?;
703    for c in cmd_buf.iter() {
704        if let RenderCommand::Draw(primitive) = c {
705            crate::draw::draw_zbuffered_2xssaa(primitive.clone(), fb, frame.zbuffer, frame.width);
706        }
707    }
708    Ok(())
709}