use concinnity_core::gfx::render_types::GpuObjectData;
use core::mem::{offset_of, size_of};
const VULKAN_SHADERS: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/src/vulkan/shaders");
const DIRECTX_SHADERS: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/src/directx/shaders");
const METAL_SHADERS: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/src/metal/shaders");
const SLANG_SHADERS: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/src/shaders");
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Kind {
Mat4,
Vec3,
Scalar,
}
impl Kind {
fn size(self) -> usize {
match self {
Kind::Mat4 => 64,
Kind::Vec3 => 12,
Kind::Scalar => 4,
}
}
}
struct Lane {
declared: &'static str,
vec3: &'static str,
scalar: &'static str,
}
struct Fragment {
label: &'static str,
source: &'static str,
mat4: &'static str,
vec3: &'static str,
vec4: Option<&'static str>,
lanes: &'static [Lane],
}
const SLANG_LANES: &[Lane] = &[
Lane {
declared: "tint_roughness",
vec3: "tint",
scalar: "roughness",
},
Lane {
declared: "emissive_metallic",
vec3: "emissive",
scalar: "metallic",
},
Lane {
declared: "bb_min_cull_distance",
vec3: "bb_min",
scalar: "cull_distance",
},
Lane {
declared: "bb_max_blend_sharpness",
vec3: "bb_max",
scalar: "secondary_blend_sharpness",
},
];
const FRAGMENTS: &[Fragment] = &[
Fragment {
label: "object_common.glsl",
source: include_str!("vulkan/shaders/object_common.glsl"),
mat4: "mat4",
vec3: "vec3",
vec4: None,
lanes: &[],
},
Fragment {
label: "object_common.hlsl",
source: include_str!("directx/shaders/object_common.hlsl"),
mat4: "float4x4",
vec3: "float3",
vec4: None,
lanes: &[],
},
Fragment {
label: "object_common.msl",
source: include_str!("metal/shaders/object_common.msl"),
mat4: "float4x4",
vec3: "packed_float3",
vec4: None,
lanes: &[],
},
Fragment {
label: "object_common.slang",
source: include_str!("shaders/object_common.slang"),
mat4: "float4x4",
vec3: "float3",
vec4: Some("float4"),
lanes: SLANG_LANES,
},
];
#[derive(Debug, PartialEq, Eq)]
struct Member {
name: String,
kind: Kind,
offset: usize,
}
impl Fragment {
fn members(&self) -> Vec<Member> {
let body = struct_body(self.source, self.label);
let mut members = Vec::new();
let mut offset = 0usize;
for decl in body.split(';').map(str::trim).filter(|d| !d.is_empty()) {
let tokens: Vec<&str> = decl.split_whitespace().collect();
let [qualifiers @ .., ty, name] = tokens.as_slice() else {
panic!("{}: cannot parse member declaration `{decl}`", self.label);
};
assert!(
qualifiers.iter().all(|q| *q == "column_major"),
"{}: unexpected qualifier on `{decl}`",
self.label
);
if self.vec4 == Some(*ty) {
let lane = self
.lanes
.iter()
.find(|lane| lane.declared == *name)
.unwrap_or_else(|| {
panic!("{}: `{decl}` packs no declared pair", self.label);
});
members.push(Member {
name: lane.vec3.to_string(),
kind: Kind::Vec3,
offset,
});
members.push(Member {
name: lane.scalar.to_string(),
kind: Kind::Scalar,
offset: offset + Kind::Vec3.size(),
});
offset += Kind::Vec3.size() + Kind::Scalar.size();
continue;
}
let kind = if *ty == self.mat4 {
Kind::Mat4
} else if *ty == self.vec3 {
Kind::Vec3
} else if *ty == "float" || *ty == "uint" {
Kind::Scalar
} else {
panic!("{}: unhandled member type `{ty}` on `{decl}`", self.label);
};
members.push(Member {
name: (*name).to_string(),
kind,
offset,
});
offset += kind.size();
}
members
}
fn size(&self) -> usize {
self.members().iter().map(|m| m.kind.size()).sum()
}
}
fn struct_body(source: &str, label: &str) -> String {
let uncommented: String = source
.lines()
.map(|line| line.split("//").next().unwrap_or(""))
.collect::<Vec<_>>()
.join("\n");
let decl = uncommented
.find("struct GpuObjectData")
.unwrap_or_else(|| panic!("{label}: no `struct GpuObjectData` declaration"));
let open = uncommented[decl..]
.find('{')
.unwrap_or_else(|| panic!("{label}: `struct GpuObjectData` has no body"))
+ decl;
let close = uncommented[open..]
.find('}')
.unwrap_or_else(|| panic!("{label}: `struct GpuObjectData` body is unterminated"))
+ open;
uncommented[open + 1..close].to_string()
}
fn rust_members() -> Vec<Member> {
let pad = offset_of!(GpuObjectData, _pad);
let mut members: Vec<Member> = [
("model", Kind::Mat4, offset_of!(GpuObjectData, model)),
("tint", Kind::Vec3, offset_of!(GpuObjectData, tint)),
(
"roughness",
Kind::Scalar,
offset_of!(GpuObjectData, roughness),
),
("emissive", Kind::Vec3, offset_of!(GpuObjectData, emissive)),
(
"metallic",
Kind::Scalar,
offset_of!(GpuObjectData, metallic),
),
(
"albedo_index",
Kind::Scalar,
offset_of!(GpuObjectData, albedo_index),
),
(
"normal_index",
Kind::Scalar,
offset_of!(GpuObjectData, normal_index),
),
(
"macro_variation",
Kind::Scalar,
offset_of!(GpuObjectData, macro_variation),
),
(
"terrain_blend",
Kind::Scalar,
offset_of!(GpuObjectData, terrain_blend),
),
("bb_min", Kind::Vec3, offset_of!(GpuObjectData, bb_min)),
(
"cull_distance",
Kind::Scalar,
offset_of!(GpuObjectData, cull_distance),
),
("bb_max", Kind::Vec3, offset_of!(GpuObjectData, bb_max)),
(
"secondary_blend_sharpness",
Kind::Scalar,
offset_of!(GpuObjectData, secondary_blend_sharpness),
),
(
"albedo_secondary_index",
Kind::Scalar,
offset_of!(GpuObjectData, albedo_secondary_index),
),
(
"normal_secondary_index",
Kind::Scalar,
offset_of!(GpuObjectData, normal_secondary_index),
),
(
"emissive_map_index",
Kind::Scalar,
offset_of!(GpuObjectData, emissive_map_index),
),
(
"orm_map_index",
Kind::Scalar,
offset_of!(GpuObjectData, orm_map_index),
),
(
"alpha_cutoff",
Kind::Scalar,
offset_of!(GpuObjectData, alpha_cutoff),
),
("_pad0", Kind::Scalar, pad),
("_pad1", Kind::Scalar, pad + 4),
("_pad2", Kind::Scalar, pad + 8),
]
.into_iter()
.map(|(name, kind, offset)| Member {
name: name.to_string(),
kind,
offset,
})
.collect();
members.sort_by_key(|m| m.offset);
members
}
#[test]
fn fragments_match_the_rust_record() {
let expected = rust_members();
for fragment in FRAGMENTS {
assert_eq!(
fragment.members(),
expected,
"{} drifted from the Rust GpuObjectData",
fragment.label
);
assert_eq!(
fragment.size(),
size_of::<GpuObjectData>(),
"{} strides at a different size",
fragment.label
);
}
}
#[test]
fn fragments_satisfy_gpu_packing_rules() {
for fragment in FRAGMENTS {
assert_eq!(
fragment.size() % 16,
0,
"{}: size is not a multiple of 16",
fragment.label
);
for member in fragment.members() {
match member.kind {
Kind::Mat4 => assert_eq!(
member.offset % 16,
0,
"{}: `{}` is a matrix at an unaligned offset {}",
fragment.label,
member.name,
member.offset
),
Kind::Vec3 => assert_eq!(
member.offset / 16,
(member.offset + member.kind.size() - 1) / 16,
"{}: `{}` straddles a 16-byte boundary at offset {}",
fragment.label,
member.name,
member.offset
),
Kind::Scalar => {}
}
}
}
}
#[test]
fn glsl_vectors_sit_on_std430_boundaries() {
let glsl = &FRAGMENTS[0];
assert_eq!(glsl.label, "object_common.glsl");
for member in glsl.members().iter().filter(|m| m.kind == Kind::Vec3) {
assert_eq!(
member.offset % 16,
0,
"{}: `{}` is a vec3 at std430-unaligned offset {}",
glsl.label,
member.name,
member.offset
);
}
}
#[test]
fn msl_three_float_members_are_packed() {
let msl = &FRAGMENTS[2];
assert_eq!(msl.label, "object_common.msl");
for decl in struct_body(msl.source, msl.label)
.split(';')
.map(str::trim)
.filter(|d| !d.is_empty())
{
assert!(
!decl.starts_with("float3 "),
"{}: `{decl}` must be packed_float3",
msl.label
);
}
}
#[test]
fn no_shader_redeclares_the_record() {
let fragments = [
"object_common.glsl",
"object_common.hlsl",
"object_common.msl",
"object_common.slang",
];
let mut declarations = 0usize;
for dir in [
VULKAN_SHADERS,
DIRECTX_SHADERS,
METAL_SHADERS,
SLANG_SHADERS,
] {
for entry in std::fs::read_dir(dir).unwrap_or_else(|e| panic!("read {dir}: {e}")) {
let path = entry.expect("dir entry").path();
let name = path
.file_name()
.and_then(|n| n.to_str())
.expect("utf8 shader filename")
.to_string();
let source = std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
if !source.contains("struct GpuObjectData") {
continue;
}
assert!(
fragments.contains(&name.as_str()),
"{name} declares its own GpuObjectData; splice the shared fragment instead"
);
declarations += 1;
}
}
assert_eq!(declarations, fragments.len(), "a fragment went missing");
}
#[test]
fn every_backend_splices_the_record() {
for dir in [VULKAN_SHADERS, DIRECTX_SHADERS, METAL_SHADERS] {
let spliced = std::fs::read_dir(dir)
.unwrap_or_else(|e| panic!("read {dir}: {e}"))
.filter(|entry| {
let path = entry.as_ref().expect("dir entry").path();
std::fs::read_to_string(&path).is_ok_and(|source| source.contains("{OBJECT_DATA}"))
})
.count();
assert!(
spliced > 0,
"{dir}: no shader carries the OBJECT_DATA marker"
);
}
}