Skip to main content

embedded_3dgfx/
lib.rs

1#![no_std]
2use camera::Camera;
3use embedded_graphics_core::pixelcolor::Rgb565;
4use embedded_graphics_core::pixelcolor::RgbColor;
5use mesh::K3dMesh;
6use mesh::RenderMode;
7use nalgebra::Matrix4;
8use nalgebra::Point2;
9use nalgebra::Point3;
10use nalgebra::Vector3;
11
12// ComplexField provides sqrt() for f32 in no_std via libm
13// It appears "unused" in tests because tests use std, but it's required for no_std builds
14#[allow(unused_imports)]
15use nalgebra::ComplexField;
16
17pub mod animation;
18pub mod billboard;
19pub mod camera;
20pub mod display_backend;
21pub mod draw;
22pub mod lut;
23pub mod mesh;
24#[cfg(feature = "std")]
25pub mod painters;
26#[cfg(feature = "perfcounter")]
27pub mod perfcounter;
28pub mod physics;
29pub mod skeleton;
30pub mod softbody;
31pub mod swapchain;
32pub mod texture;
33
34// Re-export framebuffer types from external crate for user convenience
35pub use embedded_graphics_framebuf::{
36    FrameBuf,
37    backends::{DMACapableFrameBufferBackend, EndianCorrectedBuffer, EndianCorrection},
38};
39
40pub use draw::ReadPixel;
41
42#[derive(Debug, Clone)]
43pub enum DrawPrimitive {
44    ColoredPoint(Point2<i32>, Rgb565),
45    Line([Point2<i32>; 2], Rgb565),
46    ColoredTriangle([Point2<i32>; 3], Rgb565),
47    ColoredTriangleWithDepth {
48        points: [Point2<i32>; 3],
49        depths: [f32; 3],
50        color: Rgb565,
51    },
52    GouraudTriangle {
53        points: [Point2<i32>; 3],
54        colors: [Rgb565; 3],
55    },
56    GouraudTriangleWithDepth {
57        points: [Point2<i32>; 3],
58        depths: [f32; 3],
59        colors: [Rgb565; 3],
60    },
61    TexturedTriangle {
62        points: [Point2<i32>; 3],
63        uvs: [[f32; 2]; 3],
64        texture_id: u32,
65    },
66    TexturedTriangleWithDepth {
67        points: [Point2<i32>; 3],
68        depths: [f32; 3],
69        uvs: [[f32; 2]; 3],
70        texture_id: u32,
71    },
72}
73
74pub struct K3dengine {
75    pub camera: Camera,
76    width: u16,
77    height: u16,
78}
79
80impl K3dengine {
81    pub fn new(width: u16, height: u16) -> K3dengine {
82        K3dengine {
83            camera: Camera::new(width as f32 / height as f32),
84            width,
85            height,
86        }
87    }
88
89    /// Fast frustum culling check using bounding sphere.
90    /// Returns true if the mesh should be culled (not rendered).
91    #[inline]
92    fn should_cull_mesh(&self, mesh: &K3dMesh) -> bool {
93        // Get mesh position in world space
94        let mesh_pos = mesh.get_position();
95
96        // Compute distance from camera to mesh center
97        let to_mesh = mesh_pos - self.camera.position;
98        let distance = to_mesh.norm(); // Uses libm sqrt via nalgebra
99
100        // Get squared bounding radius and compute radius
101        // This is only called once per mesh, not in the inner loop
102        let radius_sq = mesh.compute_bounding_radius_sq();
103        let radius = radius_sq.sqrt(); // Uses libm sqrt (one call per mesh is acceptable)
104
105        // Far plane culling: mesh sphere is entirely beyond far plane
106        if distance - radius > self.camera.far {
107            return true;
108        }
109
110        // Near plane culling: mesh sphere is entirely before near plane
111        if distance + radius < self.camera.near {
112            return true;
113        }
114
115        // Passed culling tests - render the mesh
116        false
117    }
118
119    #[inline(always)]
120    fn transform_point(&self, point: &[f32; 3], model_matrix: Matrix4<f32>) -> Option<Point3<i32>> {
121        let point = nalgebra::Vector4::new(point[0], point[1], point[2], 1.0);
122        let point = model_matrix * point;
123
124        if point.w < 0.0 {
125            return None;
126        }
127        if point.z < self.camera.near || point.z > self.camera.far {
128            return None;
129        }
130
131        let point = Point3::from_homogeneous(point)?;
132
133        let x = ((1.0 + point.x) * 0.5 * self.width as f32) as i32;
134        let y = ((1.0 - point.y) * 0.5 * self.height as f32) as i32;
135
136        if x < 0 || x >= self.width as i32 || y < 0 || y >= self.height as i32 {
137            return None;
138        }
139
140        Some(Point3::new(
141            x,
142            y,
143            (point.z * (self.camera.far - self.camera.near) + self.camera.near) as i32,
144        ))
145    }
146
147    #[inline(always)]
148    pub fn transform_points<const N: usize>(
149        &self,
150        indices: &[usize; N],
151        vertices: &[[f32; 3]],
152        model_matrix: Matrix4<f32>,
153    ) -> Option<[Point3<i32>; N]> {
154        let mut ret = [Point3::new(0, 0, 0); N];
155
156        for i in 0..N {
157            ret[i] = self.transform_point(&vertices[indices[i]], model_matrix)?;
158        }
159
160        Some(ret)
161    }
162
163    pub fn render<'a, MS, F>(&self, meshes: MS, mut callback: F)
164    where
165        MS: IntoIterator<Item = &'a K3dMesh<'a>>,
166        F: FnMut(DrawPrimitive),
167    {
168        for mesh in meshes {
169            if mesh.geometry.vertices.is_empty() {
170                continue;
171            }
172
173            // Frustum culling: Skip meshes that are completely outside the view frustum
174            // This can improve performance by 50-90% by avoiding transformation and rendering
175            // of off-screen objects
176            if self.should_cull_mesh(mesh) {
177                continue;
178            }
179
180            // LOD Selection: Choose geometry based on distance from camera
181            let mesh_pos = mesh.get_position();
182            let distance = (mesh_pos - self.camera.position).norm();
183            let geometry = mesh.select_lod(distance);
184
185            let transform_matrix = self.camera.vp_matrix * mesh.model_matrix;
186
187            match mesh.render_mode {
188                RenderMode::Points => {
189                    let screen_space_points = geometry
190                        .vertices
191                        .iter()
192                        .filter_map(|v| self.transform_point(v, transform_matrix));
193
194                    if geometry.colors.len() == geometry.vertices.len() {
195                        for (point, color) in screen_space_points.zip(geometry.colors) {
196                            callback(DrawPrimitive::ColoredPoint(point.xy(), *color));
197                        }
198                    } else {
199                        for point in screen_space_points {
200                            callback(DrawPrimitive::ColoredPoint(point.xy(), mesh.color));
201                        }
202                    }
203                }
204
205                RenderMode::Lines if !geometry.lines.is_empty() => {
206                    for line in geometry.lines {
207                        if let Some([p1, p2]) =
208                            self.transform_points(line, geometry.vertices, transform_matrix)
209                        {
210                            callback(DrawPrimitive::Line([p1.xy(), p2.xy()], mesh.color));
211                        }
212                    }
213                }
214
215                RenderMode::Lines if !geometry.faces.is_empty() => {
216                    for face in geometry.faces {
217                        if let Some([p1, p2, p3]) =
218                            self.transform_points(face, geometry.vertices, transform_matrix)
219                        {
220                            callback(DrawPrimitive::Line([p1.xy(), p2.xy()], mesh.color));
221                            callback(DrawPrimitive::Line([p2.xy(), p3.xy()], mesh.color));
222                            callback(DrawPrimitive::Line([p3.xy(), p1.xy()], mesh.color));
223                        }
224                    }
225                }
226
227                RenderMode::Lines => {}
228
229                RenderMode::SolidLightDir(direction) => {
230                    // Pre-compute lighting constants (once per mesh, not per face)
231                    // This optimization reduces redundant calculations in the inner loop
232                    let color_as_float = Vector3::new(
233                        mesh.color.r() as f32 / 32.0,
234                        mesh.color.g() as f32 / 64.0,
235                        mesh.color.b() as f32 / 32.0,
236                    );
237
238                    // Pre-compute ambient lighting term
239                    let ambient_color = color_as_float * 0.1;
240
241                    // Pre-compute adjusted light direction
242                    // Negate only Z component of direction to fix front/back while keeping left/right
243                    let adjusted_dir = Vector3::new(direction.x, direction.y, -direction.z);
244
245                    for (face, normal) in geometry.faces.iter().zip(geometry.normals.iter()) {
246                        //Backface culling
247                        let normal = Vector3::new(normal[0], normal[1], normal[2]);
248
249                        let transformed_normal = mesh.model_matrix.transform_vector(&normal);
250
251                        // Backface culling: cull faces pointing away from camera
252                        // This improves performance by ~50% (don't render back faces)
253                        // Z-buffer handles depth ordering, but culling avoids wasted work
254                        if self.camera.get_direction().dot(&transformed_normal) < 0.0 {
255                            continue;
256                        }
257
258                        if let Some([p1, p2, p3]) =
259                            self.transform_points(face, geometry.vertices, transform_matrix)
260                        {
261                            // Calculate lighting intensity
262                            let intensity = transformed_normal.dot(&adjusted_dir).max(0.0);
263
264                            // Compute final color using pre-computed constants
265                            let final_color = color_as_float * intensity + ambient_color;
266
267                            let final_color = Vector3::new(
268                                final_color.x.clamp(0.0, 1.0),
269                                final_color.y.clamp(0.0, 1.0),
270                                final_color.z.clamp(0.0, 1.0),
271                            );
272
273                            let color = Rgb565::new(
274                                (final_color.x * 31.0) as u8,
275                                (final_color.y * 63.0) as u8,
276                                (final_color.z * 31.0) as u8,
277                            );
278                            callback(DrawPrimitive::ColoredTriangleWithDepth {
279                                points: [p1.xy(), p2.xy(), p3.xy()],
280                                depths: [p1.z as f32, p2.z as f32, p3.z as f32],
281                                color,
282                            });
283                        }
284                    }
285                }
286
287                RenderMode::GouraudLightDir(direction) => {
288                    let color_as_float = Vector3::new(
289                        mesh.color.r() as f32 / 32.0,
290                        mesh.color.g() as f32 / 64.0,
291                        mesh.color.b() as f32 / 32.0,
292                    );
293                    let ambient_color = color_as_float * 0.1;
294                    let adjusted_dir = Vector3::new(direction.x, direction.y, -direction.z);
295
296                    for (face, face_normal) in geometry.faces.iter().zip(geometry.normals.iter()) {
297                        let fn_vec = Vector3::new(face_normal[0], face_normal[1], face_normal[2]);
298                        let transformed_fn = mesh.model_matrix.transform_vector(&fn_vec);
299
300                        if self.camera.get_direction().dot(&transformed_fn) < 0.0 {
301                            continue;
302                        }
303
304                        if let Some([p1, p2, p3]) =
305                            self.transform_points(face, geometry.vertices, transform_matrix)
306                        {
307                            // Compute per-vertex colors
308                            let vertex_colors: [Rgb565; 3] = core::array::from_fn(|k| {
309                                let vn = if !geometry.vertex_normals.is_empty() {
310                                    let vn_arr = geometry.vertex_normals[face[k]];
311                                    let vn_vec = Vector3::new(vn_arr[0], vn_arr[1], vn_arr[2]);
312                                    mesh.model_matrix.transform_vector(&vn_vec)
313                                } else {
314                                    transformed_fn
315                                };
316
317                                let intensity = vn.dot(&adjusted_dir).max(0.0);
318                                let c = color_as_float * intensity + ambient_color;
319                                Rgb565::new(
320                                    (c.x.clamp(0.0, 1.0) * 31.0) as u8,
321                                    (c.y.clamp(0.0, 1.0) * 63.0) as u8,
322                                    (c.z.clamp(0.0, 1.0) * 31.0) as u8,
323                                )
324                            });
325
326                            callback(DrawPrimitive::GouraudTriangleWithDepth {
327                                points: [p1.xy(), p2.xy(), p3.xy()],
328                                depths: [p1.z as f32, p2.z as f32, p3.z as f32],
329                                colors: vertex_colors,
330                            });
331                        }
332                    }
333                }
334
335                RenderMode::BlinnPhong {
336                    light_dir,
337                    specular_intensity,
338                    shininess,
339                } => {
340                    // Pre-compute lighting constants (once per mesh, not per face)
341                    let color_as_float = Vector3::new(
342                        mesh.color.r() as f32 / 32.0,
343                        mesh.color.g() as f32 / 64.0,
344                        mesh.color.b() as f32 / 32.0,
345                    );
346
347                    // Pre-compute ambient lighting term
348                    let ambient_color = color_as_float * 0.1;
349
350                    // Pre-compute adjusted light direction
351                    // Negate only Z component of direction to fix front/back while keeping left/right
352                    let adjusted_light_dir = Vector3::new(light_dir.x, light_dir.y, -light_dir.z);
353
354                    // Normalize light direction
355                    let light_dir_normalized = adjusted_light_dir.normalize();
356
357                    for (face, normal) in geometry.faces.iter().zip(geometry.normals.iter()) {
358                        //Backface culling
359                        let normal = Vector3::new(normal[0], normal[1], normal[2]);
360                        let transformed_normal = mesh.model_matrix.transform_vector(&normal);
361                        let normalized_normal = transformed_normal.normalize();
362
363                        // Backface culling: cull faces pointing away from camera
364                        if self.camera.get_direction().dot(&normalized_normal) < 0.0 {
365                            continue;
366                        }
367
368                        if let Some([p1, p2, p3]) =
369                            self.transform_points(face, geometry.vertices, transform_matrix)
370                        {
371                            // Calculate face center in world space for view direction
372                            let v0 = geometry.vertices[face[0]];
373                            let v1 = geometry.vertices[face[1]];
374                            let v2 = geometry.vertices[face[2]];
375                            let face_center = Point3::new(
376                                (v0[0] + v1[0] + v2[0]) / 3.0,
377                                (v0[1] + v1[1] + v2[1]) / 3.0,
378                                (v0[2] + v1[2] + v2[2]) / 3.0,
379                            );
380                            let face_center_world = mesh.model_matrix.transform_point(&face_center);
381
382                            // View direction: from face to camera
383                            let view_dir = (self.camera.position - face_center_world).normalize();
384
385                            // Blinn-Phong half vector: H = normalize(L + V)
386                            let half_vector = (light_dir_normalized + view_dir).normalize();
387
388                            // Diffuse term: N·L
389                            let diffuse_intensity =
390                                normalized_normal.dot(&light_dir_normalized).max(0.0);
391
392                            // Specular term: (N·H)^shininess
393                            let specular_term =
394                                normalized_normal.dot(&half_vector).max(0.0).powf(shininess);
395
396                            // Compute final color: ambient + diffuse + specular
397                            let diffuse_color = color_as_float * diffuse_intensity;
398                            let specular_color =
399                                Vector3::new(1.0, 1.0, 1.0) * specular_term * specular_intensity;
400                            let final_color = ambient_color + diffuse_color + specular_color;
401
402                            let final_color = Vector3::new(
403                                final_color.x.clamp(0.0, 1.0),
404                                final_color.y.clamp(0.0, 1.0),
405                                final_color.z.clamp(0.0, 1.0),
406                            );
407
408                            let color = Rgb565::new(
409                                (final_color.x * 31.0) as u8,
410                                (final_color.y * 63.0) as u8,
411                                (final_color.z * 31.0) as u8,
412                            );
413                            callback(DrawPrimitive::ColoredTriangleWithDepth {
414                                points: [p1.xy(), p2.xy(), p3.xy()],
415                                depths: [p1.z as f32, p2.z as f32, p3.z as f32],
416                                color,
417                            });
418                        }
419                    }
420                }
421
422                RenderMode::Solid => {
423                    if geometry.normals.is_empty() {
424                        for face in geometry.faces.iter() {
425                            if let Some([p1, p2, p3]) =
426                                self.transform_points(face, geometry.vertices, transform_matrix)
427                            {
428                                callback(DrawPrimitive::ColoredTriangleWithDepth {
429                                    points: [p1.xy(), p2.xy(), p3.xy()],
430                                    depths: [p1.z as f32, p2.z as f32, p3.z as f32],
431                                    color: mesh.color,
432                                });
433                            }
434                        }
435                    } else {
436                        for (face, normal) in geometry.faces.iter().zip(geometry.normals) {
437                            //Backface culling
438                            let normal = Vector3::new(normal[0], normal[1], normal[2]);
439
440                            let transformed_normal = mesh.model_matrix.transform_vector(&normal);
441
442                            // Backface culling: cull faces pointing away from camera
443                            if self.camera.get_direction().dot(&transformed_normal) < 0.0 {
444                                continue;
445                            }
446
447                            if let Some([p1, p2, p3]) =
448                                self.transform_points(face, geometry.vertices, transform_matrix)
449                            {
450                                callback(DrawPrimitive::ColoredTriangleWithDepth {
451                                    points: [p1.xy(), p2.xy(), p3.xy()],
452                                    depths: [p1.z as f32, p2.z as f32, p3.z as f32],
453                                    color: mesh.color,
454                                });
455                            }
456                        }
457                    }
458                }
459            }
460        }
461    }
462}
463
464#[cfg(test)]
465mod tests {
466    extern crate std;
467    use super::*;
468
469    #[test]
470    fn test_engine_creation() {
471        let engine = K3dengine::new(640, 480);
472        assert_eq!(engine.width, 640);
473        assert_eq!(engine.height, 480);
474        assert!((engine.camera.get_aspect_ratio() - 640.0 / 480.0).abs() < 0.001);
475    }
476
477    #[test]
478    fn test_transform_point_basic() {
479        let engine = K3dengine::new(640, 480);
480        // Use camera's VP matrix directly
481        let transform_matrix = engine.camera.vp_matrix;
482
483        // Point in front of default camera, within view frustum
484        // Default camera is at origin looking at origin, so we need a point in front
485        let point = [0.0, 0.0, -5.0];
486        let result = engine.transform_point(&point, transform_matrix);
487
488        if let Some(transformed) = result {
489            // Should be within screen bounds
490            assert!(transformed.x >= 0 && transformed.x < 640);
491            assert!(transformed.y >= 0 && transformed.y < 480);
492        }
493        // If None, the point was culled which is also valid behavior
494    }
495
496    #[test]
497    fn test_transform_point_clamps_out_of_bounds() {
498        let engine = K3dengine::new(640, 480);
499        let model_matrix = nalgebra::Matrix4::identity();
500
501        // Point way outside the viewport should be clamped/rejected
502        let point = [100.0, 100.0, -5.0];
503        let result = engine.transform_point(&point, model_matrix);
504        // Should return None because coordinates are clamped out
505        assert!(result.is_none());
506    }
507
508    #[test]
509    fn test_transform_point_behind_camera() {
510        let engine = K3dengine::new(640, 480);
511        let transform_matrix = engine.camera.vp_matrix;
512
513        // Point with positive z (behind default camera orientation)
514        let point = [0.0, 0.0, 1.0];
515        let _result = engine.transform_point(&point, transform_matrix);
516        // Point behind camera or outside frustum should return None
517        // (actual behavior depends on camera setup and projection)
518        // This test just verifies the function doesn't panic
519    }
520
521    #[test]
522    fn test_transform_point_near_plane_clipping() {
523        let engine = K3dengine::new(640, 480);
524        let model_matrix = nalgebra::Matrix4::identity();
525
526        // Point too close to camera (before near plane)
527        let point = [0.0, 0.0, -0.01];
528        let result = engine.transform_point(&point, model_matrix);
529        assert!(result.is_none());
530    }
531
532    #[test]
533    fn test_transform_point_far_plane_clipping() {
534        let engine = K3dengine::new(640, 480);
535        let model_matrix = nalgebra::Matrix4::identity();
536
537        // Point too far from camera (beyond far plane)
538        let point = [0.0, 0.0, -1000.0];
539        let result = engine.transform_point(&point, model_matrix);
540        assert!(result.is_none());
541    }
542
543    #[test]
544    fn test_transform_points_array() {
545        let engine = K3dengine::new(640, 480);
546        let transform_matrix = engine.camera.vp_matrix;
547
548        let vertices = [[0.0, 0.0, -5.0], [0.1, 0.0, -5.0], [0.0, 0.1, -5.0]];
549        let indices = [0, 1, 2];
550
551        let result = engine.transform_points(&indices, &vertices, transform_matrix);
552
553        // If transform succeeds, verify we get 3 points
554        if let Some(points) = result {
555            assert_eq!(points.len(), 3);
556        }
557        // If None, one or more points were culled which is valid
558    }
559
560    #[test]
561    fn test_render_empty_faces_mesh() {
562        let engine = K3dengine::new(640, 480);
563        let vertices = [[0.0, 0.0, -5.0]]; // At least one vertex required
564        let geometry = mesh::Geometry {
565            vertices: &vertices,
566            faces: &[],
567            colors: &[],
568            lines: &[],
569            normals: &[],
570            vertex_normals: &[],
571            uvs: &[],
572            texture_id: None,
573        };
574        let mesh = mesh::K3dMesh::new(geometry);
575
576        let mut callback_count = 0;
577        engine.render(std::iter::once(&mesh), |_| {
578            callback_count += 1;
579        });
580
581        // Mesh with no faces/lines should trigger one point callback (default is Points mode)
582        assert!(callback_count > 0);
583    }
584
585    #[test]
586    fn test_render_points_mode() {
587        let engine = K3dengine::new(640, 480);
588
589        let vertices = [[0.0, 0.0, -5.0], [0.5, 0.0, -5.0]];
590
591        let geometry = mesh::Geometry {
592            vertices: &vertices,
593            faces: &[],
594            colors: &[],
595            lines: &[],
596            normals: &[],
597            vertex_normals: &[],
598            uvs: &[],
599            texture_id: None,
600        };
601
602        let mut mesh = mesh::K3dMesh::new(geometry);
603        mesh.set_render_mode(mesh::RenderMode::Points);
604
605        let mut primitives = std::vec::Vec::new();
606        engine.render(std::iter::once(&mesh), |prim| {
607            primitives.push(prim);
608        });
609
610        // Should render points
611        assert!(primitives.len() > 0);
612        for prim in primitives {
613            assert!(matches!(prim, DrawPrimitive::ColoredPoint(_, _)));
614        }
615    }
616
617    #[test]
618    fn test_render_lines_mode_with_faces() {
619        let engine = K3dengine::new(640, 480);
620
621        let vertices = [[0.0, 0.0, -5.0], [0.5, 0.0, -5.0], [0.0, 0.5, -5.0]];
622
623        let faces = [[0, 1, 2]];
624
625        let geometry = mesh::Geometry {
626            vertices: &vertices,
627            faces: &faces,
628            colors: &[],
629            lines: &[],
630            normals: &[],
631            vertex_normals: &[],
632            uvs: &[],
633            texture_id: None,
634        };
635
636        let mut mesh = mesh::K3dMesh::new(geometry);
637        mesh.set_render_mode(mesh::RenderMode::Lines);
638
639        let mut primitives = std::vec::Vec::new();
640        engine.render(std::iter::once(&mesh), |prim| {
641            primitives.push(prim);
642        });
643
644        // Should render 3 lines (edges of triangle)
645        assert_eq!(primitives.len(), 3);
646        for prim in primitives {
647            assert!(matches!(prim, DrawPrimitive::Line(_, _)));
648        }
649    }
650
651    #[test]
652    fn test_render_gouraud_light_dir() {
653        let mut engine = K3dengine::new(640, 480);
654        engine.camera.set_position(Point3::new(0.0, 0.0, -10.0));
655        engine.camera.set_target(Point3::new(0.0, 0.0, 0.0));
656
657        let vertices = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
658        let faces = [[0, 1, 2]];
659        let normals = [[0.0, 0.0, -1.0]]; // face normal pointing toward camera
660        let vertex_normals = [[0.0, 0.0, -1.0], [0.0, 0.0, -1.0], [0.0, 0.0, -1.0]];
661
662        let geometry = mesh::Geometry {
663            vertices: &vertices,
664            faces: &faces,
665            colors: &[],
666            lines: &[],
667            normals: &normals,
668            vertex_normals: &vertex_normals,
669            uvs: &[],
670            texture_id: None,
671        };
672
673        let mut mesh = mesh::K3dMesh::new(geometry);
674        mesh.set_render_mode(mesh::RenderMode::GouraudLightDir(Vector3::new(
675            0.0, 0.0, 1.0,
676        )));
677
678        let mut primitives = std::vec::Vec::new();
679        engine.render(std::iter::once(&mesh), |prim| {
680            primitives.push(prim);
681        });
682
683        // Should emit GouraudTriangleWithDepth primitives
684        assert!(!primitives.is_empty());
685        for prim in &primitives {
686            assert!(matches!(
687                prim,
688                DrawPrimitive::GouraudTriangleWithDepth { .. }
689            ));
690        }
691    }
692}