use super::ShaderDefVal; use crate::define_atomic_id; use bevy_asset::{AssetLoader, AssetPath, Handle, LoadContext, LoadedAsset}; use bevy_reflect::{TypePath, TypeUuid}; use bevy_utils::{tracing::error, BoxedFuture}; use std::{borrow::Cow, marker::Copy}; use thiserror::Error; define_atomic_id!(ShaderId); #[derive(Error, Debug)] pub enum ShaderReflectError { #[error(transparent)] WgslParse(#[from] naga::front::wgsl::ParseError), #[cfg(feature = "shader_format_glsl")] #[error("GLSL Parse Error: {0:?}")] GlslParse(Vec), #[cfg(feature = "shader_format_spirv")] #[error(transparent)] SpirVParse(#[from] naga::front::spv::Error), #[error(transparent)] Validation(#[from] naga::WithSpan), } /// A shader, as defined by its [`ShaderSource`](wgpu::ShaderSource) and [`ShaderStage`](naga::ShaderStage) /// This is an "unprocessed" shader. It can contain preprocessor directives. #[derive(Debug, Clone, TypeUuid, TypePath)] #[uuid = "d95bc916-6c55-4de3-9622-37e7b6969fda"] pub struct Shader { pub path: String, pub source: Source, pub import_path: ShaderImport, pub imports: Vec, // extra imports not specified in the source string pub additional_imports: Vec, // any shader defs that will be included when this module is used pub shader_defs: Vec, } impl Shader { fn preprocess(source: &str, path: &str) -> (ShaderImport, Vec) { let (import_path, imports, _) = naga_oil::compose::get_preprocessor_data(source); let import_path = import_path .map(ShaderImport::Custom) .unwrap_or_else(|| ShaderImport::AssetPath(path.to_owned())); let imports = imports .into_iter() .map(|import| { if import.import.starts_with('\"') { let import = import .import .chars() .skip(1) .take_while(|c| *c != '\"') .collect(); ShaderImport::AssetPath(import) } else { ShaderImport::Custom(import.import) } }) .collect(); (import_path, imports) } pub fn from_wgsl(source: impl Into>, path: impl Into) -> Shader { let source = source.into(); let path = path.into(); let (import_path, imports) = Shader::preprocess(&source, &path); Shader { path, imports, import_path, source: Source::Wgsl(source), additional_imports: Default::default(), shader_defs: Default::default(), } } pub fn from_wgsl_with_defs( source: impl Into>, path: impl Into, shader_defs: Vec, ) -> Shader { Self { shader_defs, ..Self::from_wgsl(source, path) } } pub fn from_glsl( source: impl Into>, stage: naga::ShaderStage, path: impl Into, ) -> Shader { let source = source.into(); let path = path.into(); let (import_path, imports) = Shader::preprocess(&source, &path); Shader { path, imports, import_path, source: Source::Glsl(source, stage), additional_imports: Default::default(), shader_defs: Default::default(), } } pub fn from_spirv(source: impl Into>, path: impl Into) -> Shader { let path = path.into(); Shader { path: path.clone(), imports: Vec::new(), import_path: ShaderImport::AssetPath(path), source: Source::SpirV(source.into()), additional_imports: Default::default(), shader_defs: Default::default(), } } pub fn set_import_path>(&mut self, import_path: P) { self.import_path = ShaderImport::Custom(import_path.into()); } #[must_use] pub fn with_import_path>(mut self, import_path: P) -> Self { self.set_import_path(import_path); self } #[inline] pub fn import_path(&self) -> &ShaderImport { &self.import_path } pub fn imports(&self) -> impl ExactSizeIterator { self.imports.iter() } } impl<'a> From<&'a Shader> for naga_oil::compose::ComposableModuleDescriptor<'a> { fn from(shader: &'a Shader) -> Self { let shader_defs = shader .shader_defs .iter() .map(|def| match def { ShaderDefVal::Bool(name, b) => { (name.clone(), naga_oil::compose::ShaderDefValue::Bool(*b)) } ShaderDefVal::Int(name, i) => { (name.clone(), naga_oil::compose::ShaderDefValue::Int(*i)) } ShaderDefVal::UInt(name, i) => { (name.clone(), naga_oil::compose::ShaderDefValue::UInt(*i)) } }) .collect(); let as_name = match &shader.import_path { ShaderImport::AssetPath(asset_path) => Some(format!("\"{asset_path}\"")), ShaderImport::Custom(_) => None, }; naga_oil::compose::ComposableModuleDescriptor { source: shader.source.as_str(), file_path: &shader.path, language: (&shader.source).into(), additional_imports: &shader.additional_imports, shader_defs, as_name, } } } impl<'a> From<&'a Shader> for naga_oil::compose::NagaModuleDescriptor<'a> { fn from(shader: &'a Shader) -> Self { naga_oil::compose::NagaModuleDescriptor { source: shader.source.as_str(), file_path: &shader.path, shader_type: (&shader.source).into(), ..Default::default() } } } #[derive(Debug, Clone)] pub enum Source { Wgsl(Cow<'static, str>), Glsl(Cow<'static, str>, naga::ShaderStage), SpirV(Cow<'static, [u8]>), // TODO: consider the following // PrecompiledSpirVMacros(HashMap, Vec>) // NagaModule(Module) ... Module impls Serialize/Deserialize } impl Source { pub fn as_str(&self) -> &str { match self { Source::Wgsl(s) | Source::Glsl(s, _) => s, Source::SpirV(_) => panic!("spirv not yet implemented"), } } } impl From<&Source> for naga_oil::compose::ShaderLanguage { fn from(value: &Source) -> Self { match value { Source::Wgsl(_) => naga_oil::compose::ShaderLanguage::Wgsl, Source::Glsl(_, _) => naga_oil::compose::ShaderLanguage::Glsl, Source::SpirV(_) => panic!("spirv not yet implemented"), } } } impl From<&Source> for naga_oil::compose::ShaderType { fn from(value: &Source) -> Self { match value { Source::Wgsl(_) => naga_oil::compose::ShaderType::Wgsl, Source::Glsl(_, naga::ShaderStage::Vertex) => naga_oil::compose::ShaderType::GlslVertex, Source::Glsl(_, naga::ShaderStage::Fragment) => { naga_oil::compose::ShaderType::GlslFragment } Source::Glsl(_, naga::ShaderStage::Compute) => { panic!("glsl compute not yet implemented") } Source::SpirV(_) => panic!("spirv not yet implemented"), } } } #[derive(Default)] pub struct ShaderLoader; impl AssetLoader for ShaderLoader { fn load<'a>( &'a self, bytes: &'a [u8], load_context: &'a mut LoadContext, ) -> BoxedFuture<'a, Result<(), anyhow::Error>> { Box::pin(async move { let ext = load_context.path().extension().unwrap().to_str().unwrap(); let shader = match ext { "spv" => { Shader::from_spirv(Vec::from(bytes), load_context.path().to_string_lossy()) } "wgsl" => Shader::from_wgsl( String::from_utf8(Vec::from(bytes))?, load_context.path().to_string_lossy(), ), "vert" => Shader::from_glsl( String::from_utf8(Vec::from(bytes))?, naga::ShaderStage::Vertex, load_context.path().to_string_lossy(), ), "frag" => Shader::from_glsl( String::from_utf8(Vec::from(bytes))?, naga::ShaderStage::Fragment, load_context.path().to_string_lossy(), ), "comp" => Shader::from_glsl( String::from_utf8(Vec::from(bytes))?, naga::ShaderStage::Compute, load_context.path().to_string_lossy(), ), _ => panic!("unhandled extension: {ext}"), }; // collect file dependencies let dependencies = shader .imports .iter() .flat_map(|import| { if let ShaderImport::AssetPath(asset_path) = import { Some(asset_path.clone()) } else { None } }) .collect::>(); let mut asset = LoadedAsset::new(shader); for dependency in dependencies { asset.add_dependency(dependency.into()); } load_context.set_default_asset(asset); Ok(()) }) } fn extensions(&self) -> &[&str] { &["spv", "wgsl", "vert", "frag", "comp"] } } #[derive(Debug, PartialEq, Eq, Clone, Hash)] pub enum ShaderImport { AssetPath(String), Custom(String), } impl ShaderImport { pub fn module_name(&self) -> Cow<'_, String> { match self { ShaderImport::AssetPath(s) => Cow::Owned(format!("\"{s}\"")), ShaderImport::Custom(s) => Cow::Borrowed(s), } } } /// A reference to a shader asset. pub enum ShaderRef { /// Use the "default" shader for the current context. Default, /// A handle to a shader stored in the [`Assets`](bevy_asset::Assets) resource Handle(Handle), /// An asset path leading to a shader Path(AssetPath<'static>), } impl From> for ShaderRef { fn from(handle: Handle) -> Self { Self::Handle(handle) } } impl From> for ShaderRef { fn from(path: AssetPath<'static>) -> Self { Self::Path(path) } } impl From<&'static str> for ShaderRef { fn from(path: &'static str) -> Self { Self::Path(AssetPath::from(path)) } }