geometry_dag/
validation.rs1use crate::error::{DagError, DagResult};
4use crate::types::{
5 IndexedGeometry, MESHLET_MAX_TRIANGLES_LIMIT, MESHLET_MAX_VERTICES_LIMIT, SOURCE_TRIANGLES_BUDGET,
6 SOURCE_VERTICES_BUDGET,
7};
8
9#[derive(Debug, Clone)]
11pub struct ValidatedInput {
12 pub geometry: IndexedGeometry,
14 pub max_vertices: u32,
16 pub max_triangles: u32,
18}
19
20pub fn validate_input(
29 geometry: &IndexedGeometry,
30 max_vertices: Option<u32>,
31 max_triangles: Option<u32>,
32) -> DagResult<ValidatedInput> {
33 if !geometry.positions.len().is_multiple_of(3) {
34 return Err(DagError::invalid_input(
35 "positions must contain tightly packed XYZ triples.",
36 ));
37 }
38 if !geometry.indices.len().is_multiple_of(3) {
39 return Err(DagError::invalid_input(
40 "indices must contain complete triangles.",
41 ));
42 }
43 let vertex_count = geometry.vertex_count();
44 let triangle_count = geometry.triangle_count();
45 budget(vertex_count as u64, SOURCE_VERTICES_BUDGET, "source vertices")?;
46 budget(triangle_count as u64, SOURCE_TRIANGLES_BUDGET, "source triangles")?;
47 if geometry.positions.iter().any(|v| !v.is_finite()) {
48 return Err(DagError::invalid_input("Position components must be finite."));
49 }
50 for (offset, &index) in geometry.indices.iter().enumerate() {
51 if index as usize >= vertex_count {
52 return Err(DagError::invalid_input(format!(
53 "Index {offset} ({index}) is outside the source vertex range ({vertex_count})."
54 )));
55 }
56 }
57 let max_vertices = meshlet_limit(max_vertices, MESHLET_MAX_VERTICES_LIMIT, "maxVertices")?;
58 let max_triangles = meshlet_limit(max_triangles, MESHLET_MAX_TRIANGLES_LIMIT, "maxTriangles")?;
59 Ok(ValidatedInput { geometry: geometry.clone(), max_vertices, max_triangles })
60}
61
62pub(crate) fn budget(value: u64, maximum: u64, label: &str) -> DagResult<()> {
64 if value > maximum {
65 return Err(DagError::budget_exceeded(label, maximum));
66 }
67 Ok(())
68}
69
70fn meshlet_limit(value: Option<u32>, maximum: u32, label: &str) -> DagResult<u32> {
72 let resolved = value.unwrap_or(maximum);
73 if resolved < 1 || resolved > maximum {
74 return Err(DagError::invalid_input(format!(
75 "{label} must be an integer in 1..{maximum}."
76 )));
77 }
78 Ok(resolved)
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84
85 fn geometry(vertex_count: usize, triangle_count: usize) -> IndexedGeometry {
86 IndexedGeometry {
87 positions: vec![0.0; vertex_count * 3],
88 indices: vec![0; triangle_count * 3],
89 }
90 }
91
92 #[test]
93 fn accepts_valid_geometry() {
94 let mut g = geometry(4, 2);
95 g.indices.copy_from_slice(&[0, 1, 2, 1, 2, 3]);
96 assert!(validate_input(&g, None, None).is_ok());
97 }
98
99 #[test]
100 fn rejects_misaligned_arrays() {
101 let mut g = geometry(2, 1);
103 g.positions.truncate(5);
104 let err = validate_input(&g, None, None).unwrap_err();
105 assert!(err.to_string().contains("tightly packed"), "{err}");
106 let mut g = geometry(3, 2);
108 g.indices.truncate(5);
109 let err = validate_input(&g, None, None).unwrap_err();
110 assert!(err.to_string().contains("complete triangles"), "{err}");
111 }
112
113 #[test]
114 fn rejects_out_of_range_index() {
115 let mut g = geometry(2, 1);
116 g.indices.copy_from_slice(&[0, 1, 2]);
117 let err = validate_input(&g, None, None).unwrap_err();
118 assert!(err.to_string().contains("outside the source vertex range"));
119 }
120
121 #[test]
122 fn rejects_non_finite_positions() {
123 let mut g = geometry(1, 0);
124 g.positions[1] = f32::NAN;
125 assert!(validate_input(&g, None, None).is_err());
126 g.positions[1] = f32::INFINITY;
127 assert!(validate_input(&g, None, None).is_err());
128 }
129
130 #[test]
131 fn rejects_bad_meshlet_limits() {
132 let g = geometry(3, 1);
133 assert!(validate_input(&g, Some(0), None).is_err());
134 assert!(validate_input(&g, Some(65), None).is_err());
135 assert!(validate_input(&g, None, Some(127)).is_err());
136 assert!(validate_input(&g, Some(64), Some(126)).is_ok());
137 }
138
139 #[test]
140 fn rejects_empty_meshlet_limit() {
141 let g = geometry(0, 0);
142 assert!(validate_input(&g, Some(0), Some(0)).is_err());
143 }
144}