use std::collections::BTreeMap;
use serde_json::Value;
pub(super) struct ShaderField {
pub name: String,
pub offset: usize,
pub size: usize,
}
pub(super) struct ShaderStruct {
pub fields: Vec<ShaderField>,
pub block_size: Option<usize>,
}
impl ShaderStruct {
pub(super) fn extent(&self) -> usize {
self.fields
.iter()
.map(|f| f.offset + f.size)
.max()
.unwrap_or(0)
}
}
pub(super) fn structs(json: &str) -> Result<BTreeMap<String, ShaderStruct>, String> {
let root: Value = serde_json::from_str(json).map_err(|e| format!("reflection json: {e}"))?;
let mut fields = BTreeMap::new();
let mut blocks = BTreeMap::new();
collect(&root, &mut fields, &mut blocks);
Ok(fields
.into_iter()
.map(|(name, fields)| {
let block_size = blocks.get(&name).copied();
(name, ShaderStruct { fields, block_size })
})
.collect())
}
fn collect(
node: &Value,
fields: &mut BTreeMap<String, Vec<ShaderField>>,
blocks: &mut BTreeMap<String, usize>,
) {
match node {
Value::Object(map) => {
if let Some(name) = struct_name(map) {
let members = members(map);
if !members.is_empty() {
fields.entry(name).or_insert(members);
}
}
if let Some((name, size)) = block(map) {
blocks.entry(name).or_insert(size);
}
for value in map.values() {
collect(value, fields, blocks);
}
}
Value::Array(items) => {
for value in items {
collect(value, fields, blocks);
}
}
_ => {}
}
}
fn struct_name(map: &serde_json::Map<String, Value>) -> Option<String> {
(map.get("kind")?.as_str()? == "struct")
.then(|| map.get("name")?.as_str().map(str::to_string))
.flatten()
}
fn members(map: &serde_json::Map<String, Value>) -> Vec<ShaderField> {
let Some(fields) = map.get("fields").and_then(Value::as_array) else {
return Vec::new();
};
fields
.iter()
.filter_map(|field| {
let binding = field.get("binding")?;
(binding.get("kind")?.as_str()? == "uniform").then_some(())?;
Some(ShaderField {
name: field.get("name")?.as_str()?.to_string(),
offset: usize::try_from(binding.get("offset")?.as_u64()?).ok()?,
size: usize::try_from(binding.get("size")?.as_u64()?).ok()?,
})
})
.collect()
}
fn block(map: &serde_json::Map<String, Value>) -> Option<(String, usize)> {
(map.get("kind")?.as_str()? == "constantBuffer").then_some(())?;
let element = map.get("elementVarLayout")?;
let ty = element.get("type")?;
(ty.get("kind")?.as_str()? == "struct").then_some(())?;
let name = ty.get("name")?.as_str()?.to_string();
let size = usize::try_from(element.get("binding")?.get("size")?.as_u64()?).ok()?;
Some((name, size))
}
#[cfg(test)]
mod tests {
use super::*;
const SAMPLE: &str = r#"{
"parameters": [{
"name": "cb",
"type": {
"kind": "constantBuffer",
"elementVarLayout": {
"type": {
"kind": "struct",
"name": "Params",
"fields": [
{"name": "a", "type": {"kind": "scalar"},
"binding": {"kind": "uniform", "offset": 0, "size": 12}},
{"name": "b", "type": {"kind": "scalar"},
"binding": {"kind": "uniform", "offset": 12, "size": 4}}
]
},
"binding": {"kind": "uniform", "offset": 0, "size": 32}
}
}
}, {
"name": "vin",
"type": {
"kind": "struct",
"name": "VertexIn",
"fields": [
{"name": "pos", "binding": {"kind": "varyingInput", "index": 0}}
]
}
}]
}"#;
#[test]
fn a_constant_buffer_yields_its_fields_and_block_size() {
let found = structs(SAMPLE).expect("parse");
let params = found.get("Params").expect("Params reflected");
assert_eq!(params.block_size, Some(32));
assert_eq!(params.extent(), 16);
let names: Vec<_> = params.fields.iter().map(|f| f.name.as_str()).collect();
assert_eq!(names, ["a", "b"]);
assert_eq!(params.fields[1].offset, 12);
assert_eq!(params.fields[1].size, 4);
}
#[test]
fn a_varying_input_struct_is_not_reflected_as_a_layout() {
let found = structs(SAMPLE).expect("parse");
assert!(!found.contains_key("VertexIn"));
}
#[test]
fn malformed_json_reports_rather_than_panics() {
assert!(structs("{ not json").is_err());
}
}