1use alloc::format;
17use alloc::string::String;
18use alloc::vec;
19use alloc::vec::Vec;
20use core::mem::{offset_of, size_of};
21use hashbrown::HashMap;
22
23use crate::gfx::render_types::{
24 ClusterParams, GpuLight, GpuObjectData, LightUniforms, MaterialUniforms, ShadowPassPush,
25 ShadowUniforms, SpotShadowData,
26};
27
28use super::uniforms::ModelUniforms;
29use crate::render::uniforms::ViewUniforms;
30
31#[derive(Clone, Copy, Debug, PartialEq, Eq)]
34pub(crate) struct ExpectedField {
35 pub name: &'static str,
37 pub offset: usize,
39}
40
41#[derive(Clone, Debug)]
46pub(crate) struct ExpectedStruct {
47 pub name: &'static str,
49 pub size: usize,
51 pub fields: Vec<ExpectedField>,
53}
54
55#[derive(Clone, Copy, Debug, PartialEq, Eq)]
60pub enum EngineStage {
61 Vertex,
63 Fragment,
65 Shadow,
67}
68
69#[derive(Clone, Debug, PartialEq)]
73pub struct ReflectedStruct {
74 pub name: String,
76 pub size: usize,
79 pub fields: Vec<ReflectedField>,
81}
82
83#[derive(Clone, Debug, PartialEq)]
84pub struct ReflectedField {
86 pub name: String,
88 pub offset: usize,
90}
91
92macro_rules! field {
94 ($t:ty, $f:ident) => {
95 ExpectedField {
96 name: stringify!($f),
97 offset: offset_of!($t, $f),
98 }
99 };
100}
101
102pub(crate) fn engine_buffers(stage: EngineStage) -> Vec<(u32, ExpectedStruct)> {
111 match stage {
112 EngineStage::Vertex => vec![
113 (0, view_uniforms_layout()),
114 (2, model_uniforms_layout()),
115 (9, gpu_object_data_layout()),
116 ],
117 EngineStage::Fragment => vec![
118 (0, view_uniforms_layout()),
119 (3, material_uniforms_layout()),
120 (4, light_uniforms_layout()),
121 (5, shadow_uniforms_layout()),
122 (8, gpu_light_layout()),
123 (9, gpu_object_data_layout()),
124 (11, cluster_params_layout()),
125 (13, spot_shadow_data_layout()),
126 ],
127 EngineStage::Shadow => vec![
128 (0, shadow_uniforms_layout()),
129 (2, model_uniforms_layout()),
130 (7, shadow_pass_push_layout()),
131 ],
132 }
133}
134
135fn view_uniforms_layout() -> ExpectedStruct {
136 ExpectedStruct {
137 name: "ViewUniforms",
138 size: size_of::<ViewUniforms>(),
139 fields: vec![
140 field!(ViewUniforms, vp),
141 field!(ViewUniforms, view),
142 field!(ViewUniforms, elapsed),
143 field!(ViewUniforms, cam_pos),
144 field!(ViewUniforms, prefilter_mip_count),
145 ],
146 }
147}
148
149fn model_uniforms_layout() -> ExpectedStruct {
150 ExpectedStruct {
151 name: "ModelUniforms",
152 size: size_of::<ModelUniforms>(),
153 fields: vec![field!(ModelUniforms, model)],
154 }
155}
156
157fn gpu_object_data_layout() -> ExpectedStruct {
158 ExpectedStruct {
159 name: "GpuObjectData",
160 size: size_of::<GpuObjectData>(),
161 fields: vec![
162 field!(GpuObjectData, model),
163 field!(GpuObjectData, tint),
164 field!(GpuObjectData, roughness),
165 field!(GpuObjectData, emissive),
166 field!(GpuObjectData, metallic),
167 field!(GpuObjectData, albedo_index),
168 field!(GpuObjectData, normal_index),
169 field!(GpuObjectData, macro_variation),
170 field!(GpuObjectData, terrain_blend),
171 field!(GpuObjectData, bb_min),
172 field!(GpuObjectData, cull_distance),
173 field!(GpuObjectData, bb_max),
174 ],
175 }
176}
177
178fn material_uniforms_layout() -> ExpectedStruct {
179 ExpectedStruct {
180 name: "MaterialUniforms",
181 size: size_of::<MaterialUniforms>(),
182 fields: vec![
183 field!(MaterialUniforms, roughness),
184 field!(MaterialUniforms, metallic),
185 field!(MaterialUniforms, macro_variation),
186 field!(MaterialUniforms, terrain_blend),
187 field!(MaterialUniforms, tint),
188 field!(MaterialUniforms, emissive),
189 ],
190 }
191}
192
193fn light_uniforms_layout() -> ExpectedStruct {
194 ExpectedStruct {
195 name: "LightUniforms",
196 size: size_of::<LightUniforms>(),
197 fields: vec![
198 field!(LightUniforms, directional),
199 field!(LightUniforms, point),
200 field!(LightUniforms, num_directional),
201 field!(LightUniforms, num_point),
202 ],
203 }
204}
205
206fn gpu_light_layout() -> ExpectedStruct {
207 ExpectedStruct {
208 name: "GpuLight",
209 size: size_of::<GpuLight>(),
210 fields: vec![
211 field!(GpuLight, position),
212 field!(GpuLight, range),
213 field!(GpuLight, color),
214 field!(GpuLight, intensity),
215 field!(GpuLight, direction),
216 field!(GpuLight, kind),
217 field!(GpuLight, cos_inner),
218 field!(GpuLight, cos_outer),
219 field!(GpuLight, shadow_index),
220 ],
221 }
222}
223
224fn cluster_params_layout() -> ExpectedStruct {
225 ExpectedStruct {
226 name: "ClusterParams",
227 size: size_of::<ClusterParams>(),
228 fields: vec![
229 field!(ClusterParams, inv_view_proj),
230 field!(ClusterParams, cam_pos),
231 field!(ClusterParams, z_near),
232 field!(ClusterParams, view_forward),
233 field!(ClusterParams, z_far),
234 field!(ClusterParams, grid_x),
235 field!(ClusterParams, grid_y),
236 field!(ClusterParams, grid_z),
237 field!(ClusterParams, num_lights),
238 field!(ClusterParams, screen_w),
239 field!(ClusterParams, screen_h),
240 field!(ClusterParams, use_clusters),
241 ],
242 }
243}
244
245fn spot_shadow_data_layout() -> ExpectedStruct {
246 ExpectedStruct {
247 name: "SpotShadowData",
248 size: size_of::<SpotShadowData>(),
249 fields: vec![
250 field!(SpotShadowData, light_vp),
251 field!(SpotShadowData, depth_bias),
252 field!(SpotShadowData, normal_bias),
253 ],
254 }
255}
256
257fn shadow_uniforms_layout() -> ExpectedStruct {
258 ExpectedStruct {
259 name: "ShadowUniforms",
260 size: size_of::<ShadowUniforms>(),
261 fields: vec![
262 field!(ShadowUniforms, light_vps),
263 field!(ShadowUniforms, cascade_splits),
264 field!(ShadowUniforms, active_cascades),
265 ],
266 }
267}
268
269fn shadow_pass_push_layout() -> ExpectedStruct {
270 ExpectedStruct {
271 name: "ShadowPassPush",
272 size: size_of::<ShadowPassPush>(),
273 fields: vec![field!(ShadowPassPush, cascade_idx)],
274 }
275}
276
277pub(crate) fn compare_binding(
291 index: u32,
292 expected: &ExpectedStruct,
293 reflected: &ReflectedStruct,
294) -> Result<(), String> {
295 if expected.size != reflected.size {
296 return Err(format!(
297 "buffer({index}) binding '{}' is {} bytes but the engine's '{}' is {} bytes \
298 (a field-type mismatch such as `float3` vs `packed_float3` changes the stride \
299 and corrupts every following field / array element)",
300 reflected.name, reflected.size, expected.name, expected.size
301 ));
302 }
303 for ef in &expected.fields {
304 if let Some(rf) = reflected.fields.iter().find(|rf| rf.name == ef.name)
305 && rf.offset != ef.offset
306 {
307 return Err(format!(
308 "buffer({index}) binding '{}': field '{}' is at offset {} but the engine's \
309 '{}' puts it at offset {}",
310 reflected.name, ef.name, rf.offset, expected.name, ef.offset
311 ));
312 }
313 }
314 Ok(())
315}
316
317pub type ReflectedStructs = HashMap<u32, ReflectedStruct>;
320
321pub fn validate_stage(stage: EngineStage, reflected: &ReflectedStructs) -> Result<(), String> {
326 for (index, expected) in engine_buffers(stage) {
327 if let Some(found) = reflected.get(&index) {
328 compare_binding(index, &expected, found)?;
329 }
330 }
331 Ok(())
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337
338 use alloc::string::ToString;
339 fn faithful(expected: &ExpectedStruct) -> ReflectedStruct {
342 ReflectedStruct {
343 name: expected.name.to_string(),
344 size: expected.size,
345 fields: expected
346 .fields
347 .iter()
348 .map(|f| ReflectedField {
349 name: f.name.to_string(),
350 offset: f.offset,
351 })
352 .collect(),
353 }
354 }
355
356 fn view_expected() -> ExpectedStruct {
357 engine_buffers(EngineStage::Fragment)
358 .into_iter()
359 .find(|(i, _)| *i == 0)
360 .unwrap()
361 .1
362 }
363
364 #[test]
365 fn faithful_copy_passes_every_stage() {
366 for stage in [
369 EngineStage::Vertex,
370 EngineStage::Fragment,
371 EngineStage::Shadow,
372 ] {
373 let reflected: ReflectedStructs = engine_buffers(stage)
374 .iter()
375 .map(|(i, e)| (*i, faithful(e)))
376 .collect();
377 assert!(validate_stage(stage, &reflected).is_ok());
378 }
379 }
380
381 #[test]
382 fn unused_bindings_are_skipped() {
383 let reflected = HashMap::new();
385 assert!(validate_stage(EngineStage::Fragment, &reflected).is_ok());
386 }
387
388 #[test]
389 fn wrong_field_offset_is_rejected() {
390 let expected = view_expected();
391 let mut reflected = faithful(&expected);
392 let cam = reflected
394 .fields
395 .iter_mut()
396 .find(|f| f.name == "cam_pos")
397 .unwrap();
398 cam.offset += 4;
399 let err = compare_binding(0, &expected, &reflected).expect_err("must reject");
400 assert!(err.contains("cam_pos"), "message names the field: {err}");
401 assert!(err.contains("offset"));
402 }
403
404 #[test]
405 fn wrong_struct_size_is_rejected() {
406 let expected = view_expected();
408 let mut reflected = faithful(&expected);
409 reflected.size += 16;
410 let err = compare_binding(0, &expected, &reflected).expect_err("must reject");
411 assert!(err.contains("bytes"), "message mentions the size: {err}");
412 assert!(err.contains("stride"));
413 }
414
415 #[test]
416 fn renamed_field_is_skipped_but_size_still_guards() {
417 let expected = view_expected();
420 let mut reflected = faithful(&expected);
421 for f in &mut reflected.fields {
422 if f.name == "cam_pos" {
423 f.name = "camera_position".to_string();
424 }
425 }
426 assert!(compare_binding(0, &expected, &reflected).is_ok());
428 reflected.size += 16;
430 assert!(compare_binding(0, &expected, &reflected).is_err());
431 }
432}