diff --git a/Cargo.lock b/Cargo.lock index d63104125a..52dabd6b4c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2889,6 +2889,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", + "strum", "syn 2.0.104", ] diff --git a/Cargo.toml b/Cargo.toml index cf2a1ec0f6..a999485205 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -154,6 +154,7 @@ tinyvec = { version = "1", features = ["std"] } criterion = { version = "0.5", features = ["html_reports"] } iai-callgrind = { version = "0.12.3" } ndarray = "0.16.1" +strum = { version = "0.26.3", features = ["derive"] } [profile.dev] opt-level = 1 diff --git a/node-graph/node-macro/Cargo.toml b/node-graph/node-macro/Cargo.toml index ebc1037d92..464b4ddd0b 100644 --- a/node-graph/node-macro/Cargo.toml +++ b/node-graph/node-macro/Cargo.toml @@ -19,6 +19,7 @@ syn = { workspace = true } proc-macro2 = { workspace = true } quote = { workspace = true } convert_case = { workspace = true } +strum = { workspace = true } indoc = "2.0.5" proc-macro-crate = "3.1.0" diff --git a/node-graph/node-macro/src/codegen.rs b/node-graph/node-macro/src/codegen.rs index 456f354530..a969460e1a 100644 --- a/node-graph/node-macro/src/codegen.rs +++ b/node-graph/node-macro/src/codegen.rs @@ -345,13 +345,15 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result (#input_ident: #input_type #(, #field_idents: #field_types)*) -> #output_type #where_clause #body + #cfg #[automatically_derived] impl<'n, #(#fn_generics,)* #(#struct_generics,)* #(#future_idents,)*> #graphene_core::Node<'n, #input_type> for #mod_name::#struct_name<#(#struct_generics,)*> #struct_where_clause @@ -359,16 +361,19 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result #graphene_core::ProtoNodeIdentifier { #graphene_core::ProtoNodeIdentifier::new(std::concat!(#identifier_path, "::", std::stringify!(#struct_name))) } + #cfg #[doc(inline)] pub use #mod_name::#struct_name; #[doc(hidden)] #node_input_accessor + #cfg #[doc(hidden)] mod #mod_name { use super::*; @@ -434,7 +439,14 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result TokenStream2 { +fn generate_node_input_references( + parsed: &ParsedNodeFn, + fn_generics: &[crate::GenericParam], + field_idents: &[&PatIdent], + graphene_core: &TokenStream2, + identifier: &Ident, + cfg: &TokenStream2, +) -> TokenStream2 { let inputs_module_name = format_ident!("{}", parsed.struct_name.to_string().to_case(Case::Snake)); let mut generated_input_accessor = Vec::new(); @@ -479,6 +491,7 @@ fn generate_node_input_references(parsed: &ParsedNodeFn, fn_generics: &[crate::G } quote! { + #cfg pub mod #inputs_module_name { use super::*; diff --git a/node-graph/node-macro/src/lib.rs b/node-graph/node-macro/src/lib.rs index 1a87ce2cf9..001d3074ce 100644 --- a/node-graph/node-macro/src/lib.rs +++ b/node-graph/node-macro/src/lib.rs @@ -5,6 +5,7 @@ use syn::GenericParam; mod codegen; mod derive_choice_type; mod parsing; +mod shader_nodes; mod validation; /// Used to create a node definition. diff --git a/node-graph/node-macro/src/parsing.rs b/node-graph/node-macro/src/parsing.rs index 81151110a9..fa80828576 100644 --- a/node-graph/node-macro/src/parsing.rs +++ b/node-graph/node-macro/src/parsing.rs @@ -12,6 +12,7 @@ use syn::{ }; use crate::codegen::generate_node_code; +use crate::shader_nodes::ShaderNodeType; #[derive(Debug)] pub(crate) struct Implementation { @@ -45,6 +46,10 @@ pub(crate) struct NodeFnAttributes { pub(crate) path: Option, pub(crate) skip_impl: bool, pub(crate) properties_string: Option, + /// whether to `#[cfg]` gate the node implementation, defaults to None + pub(crate) cfg: Option, + /// if this node should get a gpu implementation, defaults to None + pub(crate) shader_node: Option, // Add more attributes as needed } @@ -184,6 +189,8 @@ impl Parse for NodeFnAttributes { let mut path = None; let mut skip_impl = false; let mut properties_string = None; + let mut cfg = None; + let mut shader_node = None; let content = input; // let content; @@ -191,8 +198,10 @@ impl Parse for NodeFnAttributes { let nested = content.call(Punctuated::::parse_terminated)?; for meta in nested { - match meta { - Meta::List(meta) if meta.path.is_ident("category") => { + let name = meta.path().get_ident().ok_or_else(|| Error::new_spanned(meta.path(), "Node macro expects a known Ident, not a path"))?; + match name.to_string().as_str() { + "category" => { + let meta = meta.require_list()?; if category.is_some() { return Err(Error::new_spanned(meta, "Multiple 'category' attributes are not allowed")); } @@ -201,14 +210,16 @@ impl Parse for NodeFnAttributes { .map_err(|_| Error::new_spanned(meta, "Expected a string literal for 'category', e.g., category(\"Value\")"))?; category = Some(lit); } - Meta::List(meta) if meta.path.is_ident("name") => { + "name" => { + let meta = meta.require_list()?; if display_name.is_some() { return Err(Error::new_spanned(meta, "Multiple 'name' attributes are not allowed")); } let parsed_name: LitStr = meta.parse_args().map_err(|_| Error::new_spanned(meta, "Expected a string for 'name', e.g., name(\"Memoize\")"))?; display_name = Some(parsed_name); } - Meta::List(meta) if meta.path.is_ident("path") => { + "path" => { + let meta = meta.require_list()?; if path.is_some() { return Err(Error::new_spanned(meta, "Multiple 'path' attributes are not allowed")); } @@ -217,13 +228,15 @@ impl Parse for NodeFnAttributes { .map_err(|_| Error::new_spanned(meta, "Expected a valid path for 'path', e.g., path(crate::MemoizeNode)"))?; path = Some(parsed_path); } - Meta::Path(path) if path.is_ident("skip_impl") => { + "skip_impl" => { + let path = meta.require_path_only()?; if skip_impl { return Err(Error::new_spanned(path, "Multiple 'skip_impl' attributes are not allowed")); } skip_impl = true; } - Meta::List(meta) if meta.path.is_ident("properties") => { + "properties" => { + let meta = meta.require_list()?; if properties_string.is_some() { return Err(Error::new_spanned(path, "Multiple 'properties_string' attributes are not allowed")); } @@ -233,13 +246,27 @@ impl Parse for NodeFnAttributes { properties_string = Some(parsed_properties_string); } + "cfg" => { + if cfg.is_some() { + return Err(Error::new_spanned(path, "Multiple 'feature' attributes are not allowed")); + } + let meta = meta.require_list()?; + cfg = Some(meta.tokens.clone()); + } + "shader_node" => { + if shader_node.is_some() { + return Err(Error::new_spanned(path, "Multiple 'feature' attributes are not allowed")); + } + let meta = meta.require_list()?; + shader_node = Some(syn::parse2(meta.tokens.to_token_stream())?); + } _ => { return Err(Error::new_spanned( meta, indoc!( r#" Unsupported attribute in `node`. - Supported attributes are 'category', 'path' and 'name'. + Supported attributes are 'category', 'path' 'name', 'skip_impl', 'cfg' and 'properties'. Example usage: #[node_macro::node(category("Value"), name("Test Node"))] @@ -256,6 +283,8 @@ impl Parse for NodeFnAttributes { path, skip_impl, properties_string, + cfg, + shader_node, }) } } @@ -758,6 +787,8 @@ mod tests { path: Some(parse_quote!(graphene_core::TestNode)), skip_impl: true, properties_string: None, + cfg: None, + shader_node: None, }, fn_name: Ident::new("add", Span::call_site()), struct_name: Ident::new("Add", Span::call_site()), @@ -819,6 +850,8 @@ mod tests { path: None, skip_impl: false, properties_string: None, + cfg: None, + shader_node: None, }, fn_name: Ident::new("transform", Span::call_site()), struct_name: Ident::new("Transform", Span::call_site()), @@ -891,6 +924,8 @@ mod tests { path: None, skip_impl: false, properties_string: None, + cfg: None, + shader_node: None, }, fn_name: Ident::new("circle", Span::call_site()), struct_name: Ident::new("Circle", Span::call_site()), @@ -948,6 +983,8 @@ mod tests { path: None, skip_impl: false, properties_string: None, + cfg: None, + shader_node: None, }, fn_name: Ident::new("levels", Span::call_site()), struct_name: Ident::new("Levels", Span::call_site()), @@ -1017,6 +1054,8 @@ mod tests { path: Some(parse_quote!(graphene_core::TestNode)), skip_impl: false, properties_string: None, + cfg: None, + shader_node: None, }, fn_name: Ident::new("add", Span::call_site()), struct_name: Ident::new("Add", Span::call_site()), @@ -1074,6 +1113,8 @@ mod tests { path: None, skip_impl: false, properties_string: None, + cfg: None, + shader_node: None, }, fn_name: Ident::new("load_image", Span::call_site()), struct_name: Ident::new("LoadImage", Span::call_site()), @@ -1131,6 +1172,8 @@ mod tests { path: None, skip_impl: false, properties_string: None, + cfg: None, + shader_node: None, }, fn_name: Ident::new("custom_node", Span::call_site()), struct_name: Ident::new("CustomNode", Span::call_site()), diff --git a/node-graph/node-macro/src/shader_nodes.rs b/node-graph/node-macro/src/shader_nodes.rs new file mode 100644 index 0000000000..919d3ef878 --- /dev/null +++ b/node-graph/node-macro/src/shader_nodes.rs @@ -0,0 +1,32 @@ +use crate::parsing::NodeFnAttributes; +use proc_macro2::{Ident, TokenStream}; +use quote::quote; +use strum::{EnumString, VariantNames}; +use syn::Error; +use syn::parse::{Parse, ParseStream}; + +pub const STD_FEATURE_GATE: &str = "std"; + +pub fn modify_cfg(attributes: &NodeFnAttributes) -> TokenStream { + match (&attributes.cfg, &attributes.shader_node) { + (Some(cfg), Some(_)) => quote!(#[cfg(all(#cfg, feature = #STD_FEATURE_GATE))]), + (Some(cfg), None) => quote!(#[cfg(#cfg)]), + (None, Some(_)) => quote!(#[cfg(feature = #STD_FEATURE_GATE)]), + (None, None) => quote!(), + } +} + +#[derive(Debug, EnumString, VariantNames)] +pub(crate) enum ShaderNodeType { + PerPixelAdjust, +} + +impl Parse for ShaderNodeType { + fn parse(input: ParseStream) -> syn::Result { + let ident: Ident = input.parse()?; + Ok(match ident.to_string().as_str() { + "PerPixelAdjust" => ShaderNodeType::PerPixelAdjust, + _ => return Err(Error::new_spanned(&ident, format!("attr 'shader_node' must be one of {:?}", Self::VARIANTS))), + }) + } +}