
# Objective AssetPath shader imports check if the shader is added using the path without quotes. this causes them to be re-added even if already present, which can cause previous dependents to get unloaded leading to a "missing import" error. ## Solution fix the module name of AssetPath shaders used for checking if it's already added to correctly use the quoted name.
341 lines
11 KiB
Rust
341 lines
11 KiB
Rust
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<naga::front::glsl::Error>),
|
|
#[cfg(feature = "shader_format_spirv")]
|
|
#[error(transparent)]
|
|
SpirVParse(#[from] naga::front::spv::Error),
|
|
#[error(transparent)]
|
|
Validation(#[from] naga::WithSpan<naga::valid::ValidationError>),
|
|
}
|
|
/// 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<ShaderImport>,
|
|
// extra imports not specified in the source string
|
|
pub additional_imports: Vec<naga_oil::compose::ImportDefinition>,
|
|
// any shader defs that will be included when this module is used
|
|
pub shader_defs: Vec<ShaderDefVal>,
|
|
}
|
|
|
|
impl Shader {
|
|
fn preprocess(source: &str, path: &str) -> (ShaderImport, Vec<ShaderImport>) {
|
|
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<Cow<'static, str>>, path: impl Into<String>) -> 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<Cow<'static, str>>,
|
|
path: impl Into<String>,
|
|
shader_defs: Vec<ShaderDefVal>,
|
|
) -> Shader {
|
|
Self {
|
|
shader_defs,
|
|
..Self::from_wgsl(source, path)
|
|
}
|
|
}
|
|
|
|
pub fn from_glsl(
|
|
source: impl Into<Cow<'static, str>>,
|
|
stage: naga::ShaderStage,
|
|
path: impl Into<String>,
|
|
) -> 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<Cow<'static, [u8]>>, path: impl Into<String>) -> 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<P: Into<String>>(&mut self, import_path: P) {
|
|
self.import_path = ShaderImport::Custom(import_path.into());
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn with_import_path<P: Into<String>>(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<Item = &ShaderImport> {
|
|
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<HashSet<String>, Vec<u32>>)
|
|
// 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::<Vec<_>>();
|
|
|
|
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<Shader>`](bevy_asset::Assets) resource
|
|
Handle(Handle<Shader>),
|
|
/// An asset path leading to a shader
|
|
Path(AssetPath<'static>),
|
|
}
|
|
|
|
impl From<Handle<Shader>> for ShaderRef {
|
|
fn from(handle: Handle<Shader>) -> Self {
|
|
Self::Handle(handle)
|
|
}
|
|
}
|
|
|
|
impl From<AssetPath<'static>> 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))
|
|
}
|
|
}
|