wgcore_derive/
lib.rs

1//! Derive proc-macros for `wgcore`.
2
3extern crate proc_macro;
4
5use darling::util::PathList;
6use darling::{FromDeriveInput, FromField};
7use proc_macro::TokenStream;
8use quote::{quote, ToTokens};
9use syn::{Data, DataStruct, Path};
10
11#[derive(FromDeriveInput, Clone)]
12#[darling(attributes(shader))]
13struct DeriveShadersArgs {
14    #[darling(default)]
15    pub derive: PathList,
16    #[darling(default)]
17    pub composable: Option<bool>,
18    pub src: String,
19    #[darling(default)]
20    pub src_fn: Option<Path>,
21    #[darling(default)]
22    pub shader_defs: Option<Path>,
23}
24
25#[derive(FromField, Clone)]
26#[darling(attributes(shader))]
27struct DeriveShadersFieldArgs {
28    #[darling(default)]
29    pub kernel: Option<String>,
30}
31
32#[proc_macro_derive(Shader, attributes(shader))]
33pub fn derive_shader(item: TokenStream) -> TokenStream {
34    let input = syn::parse_macro_input!(item as syn::DeriveInput);
35    let struct_identifier = &input.ident;
36
37    let derive_shaders = match DeriveShadersArgs::from_derive_input(&input) {
38        Ok(v) => v,
39        Err(e) => {
40            return e.write_errors().into();
41        }
42    };
43
44    match &input.data {
45        Data::Struct(DataStruct { fields, .. }) => {
46            /*
47             * Field attributes.
48             */
49            let mut kernels_to_build = vec![];
50            let src_path = derive_shaders.src;
51
52            for field in fields.iter() {
53                let field_args = match DeriveShadersFieldArgs::from_field(field) {
54                    Ok(v) => v,
55                    Err(e) => {
56                        return e.write_errors().into();
57                    }
58                };
59                let ident = field.ident.as_ref().expect("unnamed fields not supported").into_token_stream();
60                let kernel_name = field_args.kernel.map(|k| quote! { #k }).unwrap_or_else(|| quote! { stringify!(#ident) });
61
62                if fields.len() == 1 {
63                    // Don’t clone the module if there is only one field.
64                    kernels_to_build.push(quote! {
65                        #ident: wgcore::utils::load_module(device, #kernel_name, module),
66                    });
67                } else {
68                    kernels_to_build.push(quote! {
69                        #ident: wgcore::utils::load_module(device, #kernel_name, module.clone()),
70                    });
71                }
72            }
73
74            let shader_defs = derive_shaders.shader_defs.map(|defs| quote! { #defs() })
75                .unwrap_or_else(|| quote! { Default::default() });
76            let raw_src = quote! {
77                // First try to find a path from the shader registry.
78                // If doesn’t exist in the registry, try the absolute path.
79                // If it doesn’t exist in the absolute path, load the embedded string.
80                if let Some(path) = Self::absolute_path() {
81                    // TODO: handle error
82                    std::fs::read_to_string(path).unwrap()
83                } else {
84                    include_str!(#src_path).to_string()
85                }
86            };
87
88            let src = derive_shaders.src_fn.map(|f| quote! { #f(&#raw_src) })
89                .unwrap_or_else(|| quote! { #raw_src });
90            let naga_module = quote! {
91                Self::composer().and_then(|mut c|
92                    c.make_naga_module(wgcore::re_exports::naga_oil::compose::NagaModuleDescriptor {
93                        source: &Self::src(),
94                        file_path: Self::FILE_PATH,
95                        shader_defs: #shader_defs,
96                        ..Default::default()
97                    })
98                )
99            };
100
101            let from_device = if !kernels_to_build.is_empty() {
102                quote! {
103                    let module = #naga_module?;
104                    Ok(Self {
105                        #(
106                            #kernels_to_build
107                        )*
108                    })
109                }
110            } else {
111                quote ! {
112                    Ok(Self)
113                }
114            };
115
116            /*
117             * Derive shaders.
118             */
119            let to_derive: Vec<_> = derive_shaders
120                .derive
121                .iter()
122                .map(|p| p.into_token_stream())
123                .collect();
124            let composable = derive_shaders.composable.unwrap_or(true);
125            quote! {
126                #[automatically_derived]
127                impl wgcore::shader::Shader for #struct_identifier {
128                    const FILE_PATH: &'static str = #src_path;
129
130                    fn from_device(device: &wgcore::re_exports::Device) -> Result<Self, wgcore::re_exports::ComposerError> {
131                        #from_device
132                    }
133
134                    fn src() -> String {
135                        #src
136                    }
137
138                    fn naga_module() -> Result<wgcore::re_exports::wgpu::naga::Module, wgcore::re_exports::ComposerError> {
139                        #naga_module
140                    }
141
142                    fn absolute_path() -> Option<std::path::PathBuf> {
143                        if let Some(path) = wgcore::ShaderRegistry::get().get_path::<#struct_identifier>() {
144                            Some(path.clone())
145                        } else {
146                            // NOTE: this is a bit fragile, and won’t work if the current working directory
147                            //       isn’t the root of the workspace the binary crate is being run from.
148                            //       Ideally we need `proc_macro2::Span::source_file` but it is currently unstable.
149                            //       See: https://users.rust-lang.org/t/how-to-get-the-macro-called-file-path-in-a-rust-procedural-macro/109613/5
150                            std::path::Path::new(file!())
151                                .parent()?
152                                .join(Self::FILE_PATH)
153                                .canonicalize().ok()
154                        }
155                    }
156
157                    fn compose(composer: &mut wgcore::re_exports::Composer) -> Result<(), wgcore::re_exports::ComposerError> {
158                        use wgcore::composer::ComposerExt;
159                        #(
160                            #to_derive::compose(composer)?;
161                        )*
162
163                        if #composable {
164                            composer
165                                .add_composable_module_once(wgcore::re_exports::ComposableModuleDescriptor {
166                                    source: &Self::src(),
167                                    file_path: Self::FILE_PATH,
168                                    shader_defs: #shader_defs,
169                                    ..Default::default()
170                                })?;
171                        }
172
173                        Ok(())
174                    }
175
176                    /*
177                     * Hot reloading.
178                     */
179                    fn watch_sources(state: &mut wgcore::hot_reloading::HotReloadState) -> wgcore::re_exports::notify::Result<()> {
180                        #(
181                            #to_derive::watch_sources(state)?;
182                        )*
183
184                        if let Some(path) = Self::absolute_path() {
185                            state.watch_file(&path)?;
186                        }
187
188                        Ok(())
189                    }
190
191                    fn needs_reload(state: &wgcore::hot_reloading::HotReloadState) -> bool {
192                        #(
193                            if #to_derive::needs_reload(state) {
194                                return true;
195                            }
196                        )*
197
198                        Self::absolute_path()
199                            .map(|path| state.file_changed(&path))
200                            .unwrap_or_default()
201                    }
202                }
203            }
204        }
205        _ => unimplemented!(),
206    }
207    .into()
208}