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#[derive(Clone, Debug, Default)]
95pub enum ValidateShader {
96 #[default]
97 Disabled,
101 Enabled,
108}
109
110#[derive(Asset, TypePath, Debug, Clone)]
113pub struct Shader {
114 pub path: String,
116 pub source: Source,
118 pub import_path: ShaderImport,
120 pub imports: Vec<ShaderImport>,
122 pub shader_defs: Vec<ShaderDefVal>,
124 pub file_dependencies: Vec<Handle<Shader>>,
127 pub validate_shader: ValidateShader,
131}
132
133impl Shader {
134 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 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 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 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 let shader_path = std::path::Path::new("/").join(&path);
193
194 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#[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 }
231
232impl Source {
233 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#[derive(Default, TypePath)]
244pub struct ShaderLoader;
245
246#[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#[derive(serde::Serialize, serde::Deserialize, Debug, Default)]
259pub struct ShaderSettings {
260 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 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 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#[derive(Debug, PartialEq, Eq, Clone, Hash)]
348pub enum ShaderImport {
349 AssetPath(String),
351 Custom(String),
353}
354
355#[derive(Default)]
357pub enum ShaderRef {
358 #[default]
360 Default,
361 Handle(Handle<Shader>),
363 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}