Skip to main content

bevy_shader/
shader.rs

1use super::ShaderDefVal;
2use alloc::borrow::Cow;
3use bevy_asset::{io::Reader, Asset, AssetLoader, AssetPath, Handle, LoadContext};
4use bevy_reflect::TypePath;
5use bevy_utils::define_atomic_id;
6use thiserror::Error;
7
8fn scan_wesl_imports(
9    source: &str,
10    self_module_path: &wesl::syntax::ModulePath,
11) -> Vec<ShaderImport> {
12    use wesl::syntax::{ImportContent, ModulePath, PathOrigin};
13
14    fn leaves(content: &ImportContent, path: ModulePath, out: &mut Vec<ModulePath>) {
15        match content {
16            ImportContent::Item(item) => {
17                let mut full = path.clone();
18                full.push(&item.ident.to_string());
19                out.push(path);
20                out.push(full);
21            }
22            ImportContent::Collection(collection) => {
23                for import in collection {
24                    let path = path.clone().join(import.path.iter().cloned());
25                    leaves(&import.content, path, out);
26                }
27            }
28        }
29    }
30
31    let Ok(translation_unit) = source.parse::<wesl::syntax::TranslationUnit>() else {
32        return Vec::new();
33    };
34
35    let mut paths = Vec::new();
36    for statement in &translation_unit.imports {
37        match &statement.path {
38            Some(import_path) => {
39                let path = self_module_path.join_path(import_path);
40                leaves(&statement.content, path, &mut paths);
41            }
42            None => {
43                if let ImportContent::Collection(collection) = &statement.content {
44                    for import in collection {
45                        let mut components = import.path.iter().cloned();
46                        if let Some(package) = components.next() {
47                            let path =
48                                ModulePath::new(PathOrigin::Package(package), components.collect());
49                            leaves(&import.content, path, &mut paths);
50                        }
51                    }
52                }
53            }
54        }
55    }
56
57    let mut imports = Vec::new();
58    for path in &paths {
59        let path = match &path.origin {
60            PathOrigin::Package(pkg) if pkg.contains('/') => Cow::Owned(ModulePath {
61                origin: PathOrigin::Package(pkg.rsplit('/').next().unwrap().to_string()),
62                components: path.components.clone(),
63            }),
64            _ => Cow::Borrowed(path),
65        };
66        let import = match &path.origin {
67            PathOrigin::Absolute => {
68                ShaderImport::AssetPath(format!("/{}", path.components.join("/")))
69            }
70            PathOrigin::Package(package) => ShaderImport::Custom(
71                core::iter::once(package.as_str())
72                    .chain(path.components.iter().map(String::as_str))
73                    .collect::<Vec<_>>()
74                    .join("::"),
75            ),
76            PathOrigin::Relative(_) => continue,
77        };
78        if !imports.contains(&import) {
79            imports.push(import);
80        }
81    }
82    imports
83}
84
85define_atomic_id!(ShaderId);
86
87/// Describes whether or not to perform runtime checks on shaders.
88/// Runtime checks can be enabled for safety at the cost of speed.
89/// By default no runtime checks will be performed.
90///
91/// # Panics
92/// Because no runtime checks are performed for spirv,
93/// enabling `ValidateShader` for spirv will cause a panic
94#[derive(Clone, Debug, Default)]
95pub enum ValidateShader {
96    #[default]
97    /// No runtime checks for soundness (e.g. bound checking) are performed.
98    ///
99    /// This is suitable for trusted shaders, written by your program or dependencies you trust.
100    Disabled,
101    /// Enable's runtime checks for soundness (e.g. bound checking).
102    ///
103    /// While this can have a meaningful impact on performance,
104    /// this setting should *always* be enabled when loading untrusted shaders.
105    /// This might occur if you are creating a shader playground, running user-generated shaders
106    /// (as in `VRChat`), or writing a web browser in Bevy.
107    Enabled,
108}
109
110/// An "unprocessed" shader. It can contain imports and conditional
111/// compilation attributes.
112#[derive(Asset, TypePath, Debug, Clone)]
113pub struct Shader {
114    /// The asset path of the shader.
115    pub path: String,
116    /// The raw source code of the shader.
117    pub source: Source,
118    /// The path from which this shader can be imported by other shaders.
119    pub import_path: ShaderImport,
120    /// The import paths this shader depends on.
121    pub imports: Vec<ShaderImport>,
122    /// Any shader defs that should be included when this module is used.
123    pub shader_defs: Vec<ShaderDefVal>,
124    /// Strong handles to this shader's dependencies, to prevent them
125    /// from being immediately dropped if this shader is the only user.
126    pub file_dependencies: Vec<Handle<Shader>>,
127    /// Enable or disable runtime shader validation, trading safety against speed.
128    ///
129    /// Please read the [`ValidateShader`] docs for a discussion of the tradeoffs involved.
130    pub validate_shader: ValidateShader,
131}
132
133impl Shader {
134    /// Creates a new WGSL shader.
135    pub fn from_wgsl(source: impl Into<Cow<'static, str>>, path: impl Into<String>) -> Shader {
136        let source = source.into();
137        let path = path.into();
138        Shader {
139            import_path: ShaderImport::AssetPath(path.clone()),
140            path,
141            imports: Vec::new(),
142            source: Source::Wgsl(source),
143            shader_defs: Default::default(),
144            file_dependencies: Default::default(),
145            validate_shader: ValidateShader::Disabled,
146        }
147    }
148
149    /// Creates a new WGSL shader with some given shader defs.
150    pub fn from_wgsl_with_defs(
151        source: impl Into<Cow<'static, str>>,
152        path: impl Into<String>,
153        shader_defs: Vec<ShaderDefVal>,
154    ) -> Shader {
155        Self {
156            shader_defs,
157            ..Self::from_wgsl(source, path)
158        }
159    }
160
161    /// Creates a new SPIR-V shader.
162    pub fn from_spirv(source: impl Into<Cow<'static, [u8]>>, path: impl Into<String>) -> Shader {
163        let path = path.into();
164        Shader {
165            path: path.clone(),
166            imports: Vec::new(),
167            import_path: ShaderImport::AssetPath(path),
168            source: Source::SpirV(source.into()),
169            shader_defs: Default::default(),
170            file_dependencies: Default::default(),
171            validate_shader: ValidateShader::Disabled,
172        }
173    }
174
175    /// Creates a new Wesl shader.
176    pub fn from_wesl(source: impl Into<Cow<'static, str>>, path: impl Into<String>) -> Shader {
177        let source = source.into();
178        let path = path.into();
179
180        let import_path = match path.strip_prefix("embedded://") {
181            Some(embedded_path) => ShaderImport::Custom(
182                std::path::Path::new(embedded_path)
183                    .with_extension("")
184                    .to_string_lossy()
185                    .split('/')
186                    .filter(|component| !component.is_empty())
187                    .collect::<Vec<_>>()
188                    .join("::"),
189            ),
190            None => {
191                // Create the shader import path - always starting with "/"
192                let shader_path = std::path::Path::new("/").join(&path);
193
194                // Convert to a string with forward slashes and without extension
195                let import_path_str = shader_path
196                    .with_extension("")
197                    .to_string_lossy()
198                    .replace('\\', "/");
199
200                ShaderImport::AssetPath(import_path_str.to_string())
201            }
202        };
203
204        let imports = crate::shader_cache::wesl_module_path(&import_path)
205            .map(|module_path| scan_wesl_imports(&source, &module_path))
206            .unwrap_or_default();
207
208        Shader {
209            path,
210            imports,
211            import_path,
212            source: Source::Wesl(source),
213            shader_defs: Default::default(),
214            file_dependencies: Default::default(),
215            validate_shader: ValidateShader::Disabled,
216        }
217    }
218}
219
220/// Raw shader source code.
221#[expect(missing_docs, reason = "The variants are self-explanatory.")]
222#[derive(Debug, Clone)]
223pub enum Source {
224    Wgsl(Cow<'static, str>),
225    Wesl(Cow<'static, str>),
226    SpirV(Cow<'static, [u8]>),
227    // TODO: consider the following
228    // PrecompiledSpirVMacros(HashMap<HashSet<String>, Vec<u32>>)
229    // NagaModule(Module) ... Module impls Serialize/Deserialize
230}
231
232impl Source {
233    /// The underlying source code string, unless it is SPIR-V.
234    pub fn as_str(&self) -> &str {
235        match self {
236            Source::Wgsl(s) | Source::Wesl(s) => s,
237            Source::SpirV(_) => panic!("spirv not yet implemented"),
238        }
239    }
240}
241
242/// The [`AssetLoader`] responsible for loading unprocessed shader assets.
243#[derive(Default, TypePath)]
244pub struct ShaderLoader;
245
246/// An error encountered while loading a shader's source.
247#[non_exhaustive]
248#[derive(Debug, Error)]
249#[expect(missing_docs, reason = "The variants are self-explanatory.")]
250pub enum ShaderLoaderError {
251    #[error("Could not load shader: {0}")]
252    Io(#[from] std::io::Error),
253    #[error("Could not parse shader: {0}")]
254    Parse(#[from] alloc::string::FromUtf8Error),
255}
256
257/// Settings for loading shaders.
258#[derive(serde::Serialize, serde::Deserialize, Debug, Default)]
259pub struct ShaderSettings {
260    /// The shader defs to apply when this shader is loaded.
261    pub shader_defs: Vec<ShaderDefVal>,
262}
263
264impl AssetLoader for ShaderLoader {
265    type Asset = Shader;
266    type Settings = ShaderSettings;
267    type Error = ShaderLoaderError;
268    async fn load(
269        &self,
270        reader: &mut dyn Reader,
271        settings: &Self::Settings,
272        load_context: &mut LoadContext<'_>,
273    ) -> Result<Shader, Self::Error> {
274        let ext = load_context
275            .path()
276            .path()
277            .extension()
278            .unwrap()
279            .to_str()
280            .unwrap()
281            .to_string();
282        let path = load_context.path().to_string();
283        // On windows, the path will inconsistently use \ or /.
284        // TODO: remove this once AssetPath forces cross-platform "slash" consistency. See #10511
285        let path = path.replace(std::path::MAIN_SEPARATOR, "/");
286        let mut bytes = Vec::new();
287        reader.read_to_end(&mut bytes).await?;
288        if ext != "wesl" && !settings.shader_defs.is_empty() {
289            tracing::warn!(
290                "Tried to load a non-wesl shader with shader defs, this isn't supported: \
291                    The shader defs will be ignored."
292            );
293        }
294        let mut shader = match ext.as_str() {
295            "spv" => Shader::from_spirv(bytes, load_context.path().path().to_string_lossy()),
296            "wgsl" => Shader::from_wgsl(String::from_utf8(bytes)?, path),
297            "wesl" => {
298                let mut shader = Shader::from_wesl(String::from_utf8(bytes)?, path);
299                shader.shader_defs = settings.shader_defs.clone();
300                shader
301            }
302            _ => panic!("unhandled extension: {ext}"),
303        };
304
305        // collect and store file dependencies
306        match ext.as_str() {
307            "wesl" => {
308                let candidates: Vec<String> = shader
309                    .imports
310                    .iter()
311                    .filter_map(|import| match import {
312                        ShaderImport::AssetPath(asset_path) => {
313                            Some(format!("{}.{ext}", asset_path.trim_start_matches('/')))
314                        }
315                        ShaderImport::Custom(_) => None,
316                    })
317                    .collect();
318                for file_path in candidates {
319                    if load_context
320                        .read_asset_bytes(AssetPath::from(file_path.clone()))
321                        .await
322                        .is_ok()
323                    {
324                        shader
325                            .file_dependencies
326                            .push(load_context.load(AssetPath::from(file_path)));
327                    }
328                }
329            }
330            _ => {
331                for import in &shader.imports {
332                    if let ShaderImport::AssetPath(asset_path) = import {
333                        shader.file_dependencies.push(load_context.load(asset_path));
334                    }
335                }
336            }
337        }
338        Ok(shader)
339    }
340
341    fn extensions(&self) -> &[&str] {
342        &["spv", "wgsl", "wesl"]
343    }
344}
345
346/// A shader import, described as either an asset path or an import path.
347#[derive(Debug, PartialEq, Eq, Clone, Hash)]
348pub enum ShaderImport {
349    /// An asset path to a shader.
350    AssetPath(String),
351    /// An import path from which a shader may be imported.
352    Custom(String),
353}
354
355/// A reference to a shader asset.
356#[derive(Default)]
357pub enum ShaderRef {
358    /// Use the "default" shader for the current context.
359    #[default]
360    Default,
361    /// A handle to a shader stored in the [`Assets<Shader>`](bevy_asset::Assets) resource.
362    Handle(Handle<Shader>),
363    /// An asset path leading to a shader.
364    Path(AssetPath<'static>),
365}
366
367impl From<Handle<Shader>> for ShaderRef {
368    fn from(handle: Handle<Shader>) -> Self {
369        Self::Handle(handle)
370    }
371}
372
373impl From<AssetPath<'static>> for ShaderRef {
374    fn from(path: AssetPath<'static>) -> Self {
375        Self::Path(path)
376    }
377}
378
379impl From<&'static str> for ShaderRef {
380    fn from(path: &'static str) -> Self {
381        Self::Path(AssetPath::from(path))
382    }
383}