loam_render/shader/
mod.rs1#[derive(Debug, thiserror::Error)]
4pub enum WgslValidationError {
5 #[error("WGSL parse error: {0}")]
6 Parse(#[from] naga::front::wgsl::ParseError),
7 #[error("WGSL validation error: {0}")]
8 Validate(Box<naga::WithSpan<naga::valid::ValidationError>>),
9 #[error("WGSL is missing {stage:?} entry point `{name}`")]
10 EntryPoint {
11 stage: naga::ShaderStage,
12 name: &'static str,
13 },
14 #[error("WGSL binding {group}:{binding} must use {expected:?}; found {found:?}")]
15 Binding {
16 group: u32,
17 binding: u32,
18 expected: naga::AddressSpace,
19 found: Option<naga::AddressSpace>,
20 },
21}
22
23const RAYMARCH_BINDINGS: &[(u32, u32, naga::AddressSpace)] = &[(0, 0, naga::AddressSpace::Uniform)];
24const HYPERSLICE_BINDINGS: &[(u32, u32, naga::AddressSpace)] = &[
25 (0, 0, naga::AddressSpace::Uniform),
26 (
27 0,
28 1,
29 naga::AddressSpace::Storage {
30 access: naga::StorageAccess::LOAD,
31 },
32 ),
33];
34
35pub const GEODESIC_MARCH_KERNEL: &str = concat!(
37 include_str!("projective_depth.wgsl"),
38 include_str!("kernel.wgsl")
39);
40
41pub fn assemble_wgsl(modules: &[&str]) -> String {
42 let capacity = modules
43 .iter()
44 .map(|module| module.len() + usize::from(!module.ends_with('\n')))
45 .sum();
46 let mut source = String::with_capacity(capacity);
47 for module in modules {
48 source.push_str(module);
49 if !module.ends_with('\n') {
50 source.push('\n');
51 }
52 }
53 source
54}
55
56pub fn validate_wgsl(source: &str) -> Result<(), WgslValidationError> {
57 parse_and_validate(source).map(|_| ())
58}
59
60fn parse_and_validate(source: &str) -> Result<naga::Module, WgslValidationError> {
61 let module = naga::front::wgsl::parse_str(source)?;
62 let flags = naga::valid::ValidationFlags::all();
63 let caps = naga::valid::Capabilities::empty();
64 naga::valid::Validator::new(flags, caps)
65 .validate(&module)
66 .map_err(|error| WgslValidationError::Validate(Box::new(error)))?;
67 Ok(module)
68}
69
70pub(crate) fn validate_raymarch_wgsl(source: &str) -> Result<(), WgslValidationError> {
71 validate_render_wgsl(source, &["fs_main"], RAYMARCH_BINDINGS)
72}
73
74pub(crate) fn validate_hyperslice_wgsl(source: &str) -> Result<(), WgslValidationError> {
75 validate_render_wgsl(source, &["fs_main", "fs_depth"], HYPERSLICE_BINDINGS)
76}
77
78fn validate_render_wgsl(
79 source: &str,
80 fragment_entries: &[&'static str],
81 required_bindings: &[(u32, u32, naga::AddressSpace)],
82) -> Result<(), WgslValidationError> {
83 let module = parse_and_validate(source)?;
84 for &(group, binding, expected) in required_bindings {
85 let found = module.global_variables.iter().find_map(|(_, variable)| {
86 (variable.binding == Some(naga::ResourceBinding { group, binding }))
87 .then_some(variable.space)
88 });
89 if found != Some(expected) {
90 return Err(WgslValidationError::Binding {
91 group,
92 binding,
93 expected,
94 found,
95 });
96 }
97 }
98 for (stage, name) in std::iter::once((naga::ShaderStage::Vertex, "vs_fullscreen")).chain(
99 fragment_entries
100 .iter()
101 .copied()
102 .map(|name| (naga::ShaderStage::Fragment, name)),
103 ) {
104 if !module
105 .entry_points
106 .iter()
107 .any(|entry| entry.stage == stage && entry.name == name)
108 {
109 return Err(WgslValidationError::EntryPoint { stage, name });
110 }
111 }
112 Ok(())
113}