use alloc::format;
use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;
use core::mem::{offset_of, size_of};
use hashbrown::HashMap;
use crate::render_types::{
ClusterParams, GpuLight, GpuObjectData, LightUniforms, MaterialUniforms, ShadowPassPush,
ShadowUniforms, SpotShadowData,
};
use super::uniforms::ModelUniforms;
use crate::uniforms::ViewUniforms;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct ExpectedField {
pub name: &'static str,
pub offset: usize,
}
#[derive(Clone, Debug)]
pub(crate) struct ExpectedStruct {
pub name: &'static str,
pub size: usize,
pub fields: Vec<ExpectedField>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum EngineStage {
Vertex,
Fragment,
Shadow,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ReflectedStruct {
pub name: String,
pub size: usize,
pub fields: Vec<ReflectedField>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ReflectedField {
pub name: String,
pub offset: usize,
}
macro_rules! field {
($t:ty, $f:ident) => {
ExpectedField {
name: stringify!($f),
offset: offset_of!($t, $f),
}
};
}
pub(crate) fn engine_buffers(stage: EngineStage) -> Vec<(u32, ExpectedStruct)> {
match stage {
EngineStage::Vertex => vec![
(0, view_uniforms_layout()),
(2, model_uniforms_layout()),
(9, gpu_object_data_layout()),
],
EngineStage::Fragment => vec![
(0, view_uniforms_layout()),
(3, material_uniforms_layout()),
(4, light_uniforms_layout()),
(5, shadow_uniforms_layout()),
(8, gpu_light_layout()),
(9, gpu_object_data_layout()),
(11, cluster_params_layout()),
(13, spot_shadow_data_layout()),
],
EngineStage::Shadow => vec![
(0, shadow_uniforms_layout()),
(2, model_uniforms_layout()),
(7, shadow_pass_push_layout()),
],
}
}
fn view_uniforms_layout() -> ExpectedStruct {
ExpectedStruct {
name: "ViewUniforms",
size: size_of::<ViewUniforms>(),
fields: vec![
field!(ViewUniforms, vp),
field!(ViewUniforms, view),
field!(ViewUniforms, elapsed),
field!(ViewUniforms, cam_pos),
field!(ViewUniforms, prefilter_mip_count),
],
}
}
fn model_uniforms_layout() -> ExpectedStruct {
ExpectedStruct {
name: "ModelUniforms",
size: size_of::<ModelUniforms>(),
fields: vec![field!(ModelUniforms, model)],
}
}
fn gpu_object_data_layout() -> ExpectedStruct {
ExpectedStruct {
name: "GpuObjectData",
size: size_of::<GpuObjectData>(),
fields: vec![
field!(GpuObjectData, model),
field!(GpuObjectData, tint),
field!(GpuObjectData, roughness),
field!(GpuObjectData, emissive),
field!(GpuObjectData, metallic),
field!(GpuObjectData, albedo_index),
field!(GpuObjectData, normal_index),
field!(GpuObjectData, macro_variation),
field!(GpuObjectData, terrain_blend),
field!(GpuObjectData, bb_min),
field!(GpuObjectData, cull_distance),
field!(GpuObjectData, bb_max),
],
}
}
fn material_uniforms_layout() -> ExpectedStruct {
ExpectedStruct {
name: "MaterialUniforms",
size: size_of::<MaterialUniforms>(),
fields: vec![
field!(MaterialUniforms, roughness),
field!(MaterialUniforms, metallic),
field!(MaterialUniforms, macro_variation),
field!(MaterialUniforms, terrain_blend),
field!(MaterialUniforms, tint),
field!(MaterialUniforms, emissive),
],
}
}
fn light_uniforms_layout() -> ExpectedStruct {
ExpectedStruct {
name: "LightUniforms",
size: size_of::<LightUniforms>(),
fields: vec![
field!(LightUniforms, directional),
field!(LightUniforms, point),
field!(LightUniforms, num_directional),
field!(LightUniforms, num_point),
],
}
}
fn gpu_light_layout() -> ExpectedStruct {
ExpectedStruct {
name: "GpuLight",
size: size_of::<GpuLight>(),
fields: vec![
field!(GpuLight, position),
field!(GpuLight, range),
field!(GpuLight, color),
field!(GpuLight, intensity),
field!(GpuLight, direction),
field!(GpuLight, kind),
field!(GpuLight, cos_inner),
field!(GpuLight, cos_outer),
field!(GpuLight, shadow_index),
],
}
}
fn cluster_params_layout() -> ExpectedStruct {
ExpectedStruct {
name: "ClusterParams",
size: size_of::<ClusterParams>(),
fields: vec![
field!(ClusterParams, inv_view_proj),
field!(ClusterParams, cam_pos),
field!(ClusterParams, z_near),
field!(ClusterParams, view_forward),
field!(ClusterParams, z_far),
field!(ClusterParams, grid_x),
field!(ClusterParams, grid_y),
field!(ClusterParams, grid_z),
field!(ClusterParams, num_lights),
field!(ClusterParams, screen_w),
field!(ClusterParams, screen_h),
field!(ClusterParams, use_clusters),
],
}
}
fn spot_shadow_data_layout() -> ExpectedStruct {
ExpectedStruct {
name: "SpotShadowData",
size: size_of::<SpotShadowData>(),
fields: vec![
field!(SpotShadowData, light_vp),
field!(SpotShadowData, depth_bias),
field!(SpotShadowData, normal_bias),
],
}
}
fn shadow_uniforms_layout() -> ExpectedStruct {
ExpectedStruct {
name: "ShadowUniforms",
size: size_of::<ShadowUniforms>(),
fields: vec![
field!(ShadowUniforms, light_vps),
field!(ShadowUniforms, cascade_splits),
field!(ShadowUniforms, active_cascades),
],
}
}
fn shadow_pass_push_layout() -> ExpectedStruct {
ExpectedStruct {
name: "ShadowPassPush",
size: size_of::<ShadowPassPush>(),
fields: vec![field!(ShadowPassPush, cascade_idx)],
}
}
pub(crate) fn compare_binding(
index: u32,
expected: &ExpectedStruct,
reflected: &ReflectedStruct,
) -> Result<(), String> {
if expected.size != reflected.size {
return Err(format!(
"buffer({index}) binding '{}' is {} bytes but the engine's '{}' is {} bytes \
(a field-type mismatch such as `float3` vs `packed_float3` changes the stride \
and corrupts every following field / array element)",
reflected.name, reflected.size, expected.name, expected.size
));
}
for ef in &expected.fields {
if let Some(rf) = reflected.fields.iter().find(|rf| rf.name == ef.name)
&& rf.offset != ef.offset
{
return Err(format!(
"buffer({index}) binding '{}': field '{}' is at offset {} but the engine's \
'{}' puts it at offset {}",
reflected.name, ef.name, rf.offset, expected.name, ef.offset
));
}
}
Ok(())
}
pub type ReflectedStructs = HashMap<u32, ReflectedStruct>;
pub fn validate_stage(stage: EngineStage, reflected: &ReflectedStructs) -> Result<(), String> {
for (index, expected) in engine_buffers(stage) {
if let Some(found) = reflected.get(&index) {
compare_binding(index, &expected, found)?;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::string::ToString;
fn faithful(expected: &ExpectedStruct) -> ReflectedStruct {
ReflectedStruct {
name: expected.name.to_string(),
size: expected.size,
fields: expected
.fields
.iter()
.map(|f| ReflectedField {
name: f.name.to_string(),
offset: f.offset,
})
.collect(),
}
}
fn view_expected() -> ExpectedStruct {
engine_buffers(EngineStage::Fragment)
.into_iter()
.find(|(i, _)| *i == 0)
.unwrap()
.1
}
#[test]
fn faithful_copy_passes_every_stage() {
for stage in [
EngineStage::Vertex,
EngineStage::Fragment,
EngineStage::Shadow,
] {
let reflected: ReflectedStructs = engine_buffers(stage)
.iter()
.map(|(i, e)| (*i, faithful(e)))
.collect();
assert!(validate_stage(stage, &reflected).is_ok());
}
}
#[test]
fn unused_bindings_are_skipped() {
let reflected = HashMap::new();
assert!(validate_stage(EngineStage::Fragment, &reflected).is_ok());
}
#[test]
fn wrong_field_offset_is_rejected() {
let expected = view_expected();
let mut reflected = faithful(&expected);
let cam = reflected
.fields
.iter_mut()
.find(|f| f.name == "cam_pos")
.unwrap();
cam.offset += 4;
let err = compare_binding(0, &expected, &reflected).expect_err("must reject");
assert!(err.contains("cam_pos"), "message names the field: {err}");
assert!(err.contains("offset"));
}
#[test]
fn wrong_struct_size_is_rejected() {
let expected = view_expected();
let mut reflected = faithful(&expected);
reflected.size += 16;
let err = compare_binding(0, &expected, &reflected).expect_err("must reject");
assert!(err.contains("bytes"), "message mentions the size: {err}");
assert!(err.contains("stride"));
}
#[test]
fn renamed_field_is_skipped_but_size_still_guards() {
let expected = view_expected();
let mut reflected = faithful(&expected);
for f in &mut reflected.fields {
if f.name == "cam_pos" {
f.name = "camera_position".to_string();
}
}
assert!(compare_binding(0, &expected, &reflected).is_ok());
reflected.size += 16;
assert!(compare_binding(0, &expected, &reflected).is_err());
}
}