Skip to main content

loam_render/shader/
mod.rs

1//! Callers pass WGSL modules in dependency order.
2
3#[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
35/// Assemble after the space and scene modules.
36pub 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}