use henad_core::authoring::model::binding::RESERVED;
pub(crate) fn declared_workgroup_size(source: &str) -> Option<[u32; 3]> {
let entry = source.find("fn main(")?;
let before = &source[..entry];
let attribute = before.rfind("@workgroup_size(")?;
if before[attribute..].contains("fn ") {
return None;
}
let arguments = &before[attribute + "@workgroup_size(".len()..];
let arguments = &arguments[..arguments.find(')')?];
let mut size = [1u32; 3];
let mut count = 0;
for argument in arguments
.split(',')
.map(str::trim)
.filter(|argument| !argument.is_empty())
{
let digits = argument.trim_end_matches(['u', 'i']);
*size.get_mut(count)? = digits.parse().ok()?;
count += 1;
}
(count > 0).then_some(size)
}
pub(crate) fn assert_workgroup_size(model_id: &str, pass: &str, shader: &str, dispatched: [u32; 3]) {
if let Some(declared) = declared_workgroup_size(shader) {
assert!(
declared == dispatched,
"{model_id}: the {pass} shader declares @workgroup_size({}, {}, {}), and the engine dispatches it in \
workgroups of ({}, {}, {})",
declared[0],
declared[1],
declared[2],
dispatched[0],
dispatched[1],
dispatched[2],
);
}
}
pub(crate) fn assert_buffer_labels<'a>(model_id: &str, labels: impl IntoIterator<Item = &'a str>) {
for label in labels {
assert!(
!RESERVED.contains(&label),
"{model_id}: buffer label `{label}` is reserved, and a binding of that name binds the engine's own \
resource"
);
assert!(
!label.ends_with("_in") && !label.ends_with("_out"),
"{model_id}: buffer label `{label}` ends in `_in` or `_out`. The engine strips that suffix from a binding \
name before it looks the label up"
);
}
}
#[cfg(test)]
mod tests {
use super::{assert_buffer_labels, assert_workgroup_size, declared_workgroup_size};
#[test]
fn a_composed_entry_point_reads_its_workgroup_size() {
let composed = "struct Params {\n n: u32,\n}\n\n@compute @workgroup_size(16, 16, 1) \nfn main(\
@builtin(global_invocation_id) gid: vec3<u32>) {\n}\n";
assert_eq!(declared_workgroup_size(composed), Some([16, 16, 1]));
assert_eq!(
declared_workgroup_size("@compute @workgroup_size(256u) fn main() {}"),
Some([256, 1, 1])
);
assert_eq!(
declared_workgroup_size("@compute @workgroup_size(8, 4) fn main() {}"),
Some([8, 4, 1])
);
}
#[test]
fn a_size_that_is_not_a_literal_is_left_unread() {
assert_eq!(
declared_workgroup_size("@compute @workgroup_size(WG, WG) fn main() {}"),
None
);
assert_eq!(declared_workgroup_size("fn main() {}"), None);
assert_eq!(
declared_workgroup_size("@compute @workgroup_size(64) fn helper() {}\nfn main() {}"),
None,
"the attribute belongs to another function"
);
}
#[test]
#[should_panic(expected = "dispatches it in workgroups of (16, 16, 1)")]
fn a_mismatched_workgroup_size_is_refused() {
assert_workgroup_size(
"test",
"step",
"@compute @workgroup_size(8, 8, 1) fn main() {}",
[16, 16, 1],
);
}
#[test]
fn a_matching_workgroup_size_passes() {
assert_workgroup_size(
"test",
"step",
"@compute @workgroup_size(16, 16, 1) fn main() {}",
[16, 16, 1],
);
}
#[test]
#[should_panic(expected = "buffer label `partials` is reserved")]
fn a_reserved_label_is_refused() {
assert_buffer_labels("test", ["state", "partials"]);
}
#[test]
#[should_panic(expected = "buffer label `food_in` ends in `_in` or `_out`")]
fn a_label_with_a_side_suffix_is_refused() {
assert_buffer_labels("test", ["food_in"]);
}
#[test]
fn plain_labels_pass() {
assert_buffer_labels("test", ["state", "rng", "inbox", "outcome"]);
}
}