Shaders: node macro (#2923)

* node_macro: cleanup attr parsing

* node_macro: add `cfg()` attr to feature gate node impl

* node_macro: add `shader_nodes` option

* node_macro: fixup tests
This commit is contained in:
Firestar99 2025-07-24 14:58:30 +02:00 committed by GitHub
parent 2d11d96b4a
commit 59f3835c5d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 101 additions and 9 deletions

1
Cargo.lock generated
View file

@ -2889,6 +2889,7 @@ dependencies = [
"proc-macro-error2", "proc-macro-error2",
"proc-macro2", "proc-macro2",
"quote", "quote",
"strum",
"syn 2.0.104", "syn 2.0.104",
] ]

View file

@ -154,6 +154,7 @@ tinyvec = { version = "1", features = ["std"] }
criterion = { version = "0.5", features = ["html_reports"] } criterion = { version = "0.5", features = ["html_reports"] }
iai-callgrind = { version = "0.12.3" } iai-callgrind = { version = "0.12.3" }
ndarray = "0.16.1" ndarray = "0.16.1"
strum = { version = "0.26.3", features = ["derive"] }
[profile.dev] [profile.dev]
opt-level = 1 opt-level = 1

View file

@ -19,6 +19,7 @@ syn = { workspace = true }
proc-macro2 = { workspace = true } proc-macro2 = { workspace = true }
quote = { workspace = true } quote = { workspace = true }
convert_case = { workspace = true } convert_case = { workspace = true }
strum = { workspace = true }
indoc = "2.0.5" indoc = "2.0.5"
proc-macro-crate = "3.1.0" proc-macro-crate = "3.1.0"

View file

@ -345,13 +345,15 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStre
let properties = &attributes.properties_string.as_ref().map(|value| quote!(Some(#value))).unwrap_or(quote!(None)); let properties = &attributes.properties_string.as_ref().map(|value| quote!(Some(#value))).unwrap_or(quote!(None));
let node_input_accessor = generate_node_input_references(parsed, fn_generics, &field_idents, &graphene_core, &identifier); let cfg = crate::shader_nodes::modify_cfg(&attributes);
let node_input_accessor = generate_node_input_references(parsed, fn_generics, &field_idents, &graphene_core, &identifier, &cfg);
Ok(quote! { Ok(quote! {
/// Underlying implementation for [#struct_name] /// Underlying implementation for [#struct_name]
#[inline] #[inline]
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
#vis #async_keyword fn #fn_name <'n, #(#fn_generics,)*> (#input_ident: #input_type #(, #field_idents: #field_types)*) -> #output_type #where_clause #body #vis #async_keyword fn #fn_name <'n, #(#fn_generics,)*> (#input_ident: #input_type #(, #field_idents: #field_types)*) -> #output_type #where_clause #body
#cfg
#[automatically_derived] #[automatically_derived]
impl<'n, #(#fn_generics,)* #(#struct_generics,)* #(#future_idents,)*> #graphene_core::Node<'n, #input_type> for #mod_name::#struct_name<#(#struct_generics,)*> impl<'n, #(#fn_generics,)* #(#struct_generics,)* #(#future_idents,)*> #graphene_core::Node<'n, #input_type> for #mod_name::#struct_name<#(#struct_generics,)*>
#struct_where_clause #struct_where_clause
@ -359,16 +361,19 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStre
#eval_impl #eval_impl
} }
#cfg
const fn #identifier() -> #graphene_core::ProtoNodeIdentifier { const fn #identifier() -> #graphene_core::ProtoNodeIdentifier {
#graphene_core::ProtoNodeIdentifier::new(std::concat!(#identifier_path, "::", std::stringify!(#struct_name))) #graphene_core::ProtoNodeIdentifier::new(std::concat!(#identifier_path, "::", std::stringify!(#struct_name)))
} }
#cfg
#[doc(inline)] #[doc(inline)]
pub use #mod_name::#struct_name; pub use #mod_name::#struct_name;
#[doc(hidden)] #[doc(hidden)]
#node_input_accessor #node_input_accessor
#cfg
#[doc(hidden)] #[doc(hidden)]
mod #mod_name { mod #mod_name {
use super::*; use super::*;
@ -434,7 +439,14 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStre
} }
/// Generates strongly typed utilites to access inputs /// Generates strongly typed utilites to access inputs
fn generate_node_input_references(parsed: &ParsedNodeFn, fn_generics: &[crate::GenericParam], field_idents: &[&PatIdent], graphene_core: &TokenStream2, identifier: &Ident) -> 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 inputs_module_name = format_ident!("{}", parsed.struct_name.to_string().to_case(Case::Snake));
let mut generated_input_accessor = Vec::new(); let mut generated_input_accessor = Vec::new();
@ -479,6 +491,7 @@ fn generate_node_input_references(parsed: &ParsedNodeFn, fn_generics: &[crate::G
} }
quote! { quote! {
#cfg
pub mod #inputs_module_name { pub mod #inputs_module_name {
use super::*; use super::*;

View file

@ -5,6 +5,7 @@ use syn::GenericParam;
mod codegen; mod codegen;
mod derive_choice_type; mod derive_choice_type;
mod parsing; mod parsing;
mod shader_nodes;
mod validation; mod validation;
/// Used to create a node definition. /// Used to create a node definition.

View file

@ -12,6 +12,7 @@ use syn::{
}; };
use crate::codegen::generate_node_code; use crate::codegen::generate_node_code;
use crate::shader_nodes::ShaderNodeType;
#[derive(Debug)] #[derive(Debug)]
pub(crate) struct Implementation { pub(crate) struct Implementation {
@ -45,6 +46,10 @@ pub(crate) struct NodeFnAttributes {
pub(crate) path: Option<Path>, pub(crate) path: Option<Path>,
pub(crate) skip_impl: bool, pub(crate) skip_impl: bool,
pub(crate) properties_string: Option<LitStr>, pub(crate) properties_string: Option<LitStr>,
/// whether to `#[cfg]` gate the node implementation, defaults to None
pub(crate) cfg: Option<TokenStream2>,
/// if this node should get a gpu implementation, defaults to None
pub(crate) shader_node: Option<ShaderNodeType>,
// Add more attributes as needed // Add more attributes as needed
} }
@ -184,6 +189,8 @@ impl Parse for NodeFnAttributes {
let mut path = None; let mut path = None;
let mut skip_impl = false; let mut skip_impl = false;
let mut properties_string = None; let mut properties_string = None;
let mut cfg = None;
let mut shader_node = None;
let content = input; let content = input;
// let content; // let content;
@ -191,8 +198,10 @@ impl Parse for NodeFnAttributes {
let nested = content.call(Punctuated::<Meta, Comma>::parse_terminated)?; let nested = content.call(Punctuated::<Meta, Comma>::parse_terminated)?;
for meta in nested { for meta in nested {
match meta { let name = meta.path().get_ident().ok_or_else(|| Error::new_spanned(meta.path(), "Node macro expects a known Ident, not a path"))?;
Meta::List(meta) if meta.path.is_ident("category") => { match name.to_string().as_str() {
"category" => {
let meta = meta.require_list()?;
if category.is_some() { if category.is_some() {
return Err(Error::new_spanned(meta, "Multiple 'category' attributes are not allowed")); 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\")"))?; .map_err(|_| Error::new_spanned(meta, "Expected a string literal for 'category', e.g., category(\"Value\")"))?;
category = Some(lit); category = Some(lit);
} }
Meta::List(meta) if meta.path.is_ident("name") => { "name" => {
let meta = meta.require_list()?;
if display_name.is_some() { if display_name.is_some() {
return Err(Error::new_spanned(meta, "Multiple 'name' attributes are not allowed")); 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\")"))?; 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); display_name = Some(parsed_name);
} }
Meta::List(meta) if meta.path.is_ident("path") => { "path" => {
let meta = meta.require_list()?;
if path.is_some() { if path.is_some() {
return Err(Error::new_spanned(meta, "Multiple 'path' attributes are not allowed")); 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)"))?; .map_err(|_| Error::new_spanned(meta, "Expected a valid path for 'path', e.g., path(crate::MemoizeNode)"))?;
path = Some(parsed_path); path = Some(parsed_path);
} }
Meta::Path(path) if path.is_ident("skip_impl") => { "skip_impl" => {
let path = meta.require_path_only()?;
if skip_impl { if skip_impl {
return Err(Error::new_spanned(path, "Multiple 'skip_impl' attributes are not allowed")); return Err(Error::new_spanned(path, "Multiple 'skip_impl' attributes are not allowed"));
} }
skip_impl = true; skip_impl = true;
} }
Meta::List(meta) if meta.path.is_ident("properties") => { "properties" => {
let meta = meta.require_list()?;
if properties_string.is_some() { if properties_string.is_some() {
return Err(Error::new_spanned(path, "Multiple 'properties_string' attributes are not allowed")); 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); 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( return Err(Error::new_spanned(
meta, meta,
indoc!( indoc!(
r#" r#"
Unsupported attribute in `node`. Unsupported attribute in `node`.
Supported attributes are 'category', 'path' and 'name'. Supported attributes are 'category', 'path' 'name', 'skip_impl', 'cfg' and 'properties'.
Example usage: Example usage:
#[node_macro::node(category("Value"), name("Test Node"))] #[node_macro::node(category("Value"), name("Test Node"))]
@ -256,6 +283,8 @@ impl Parse for NodeFnAttributes {
path, path,
skip_impl, skip_impl,
properties_string, properties_string,
cfg,
shader_node,
}) })
} }
} }
@ -758,6 +787,8 @@ mod tests {
path: Some(parse_quote!(graphene_core::TestNode)), path: Some(parse_quote!(graphene_core::TestNode)),
skip_impl: true, skip_impl: true,
properties_string: None, properties_string: None,
cfg: None,
shader_node: None,
}, },
fn_name: Ident::new("add", Span::call_site()), fn_name: Ident::new("add", Span::call_site()),
struct_name: Ident::new("Add", Span::call_site()), struct_name: Ident::new("Add", Span::call_site()),
@ -819,6 +850,8 @@ mod tests {
path: None, path: None,
skip_impl: false, skip_impl: false,
properties_string: None, properties_string: None,
cfg: None,
shader_node: None,
}, },
fn_name: Ident::new("transform", Span::call_site()), fn_name: Ident::new("transform", Span::call_site()),
struct_name: Ident::new("Transform", Span::call_site()), struct_name: Ident::new("Transform", Span::call_site()),
@ -891,6 +924,8 @@ mod tests {
path: None, path: None,
skip_impl: false, skip_impl: false,
properties_string: None, properties_string: None,
cfg: None,
shader_node: None,
}, },
fn_name: Ident::new("circle", Span::call_site()), fn_name: Ident::new("circle", Span::call_site()),
struct_name: Ident::new("Circle", Span::call_site()), struct_name: Ident::new("Circle", Span::call_site()),
@ -948,6 +983,8 @@ mod tests {
path: None, path: None,
skip_impl: false, skip_impl: false,
properties_string: None, properties_string: None,
cfg: None,
shader_node: None,
}, },
fn_name: Ident::new("levels", Span::call_site()), fn_name: Ident::new("levels", Span::call_site()),
struct_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)), path: Some(parse_quote!(graphene_core::TestNode)),
skip_impl: false, skip_impl: false,
properties_string: None, properties_string: None,
cfg: None,
shader_node: None,
}, },
fn_name: Ident::new("add", Span::call_site()), fn_name: Ident::new("add", Span::call_site()),
struct_name: Ident::new("Add", Span::call_site()), struct_name: Ident::new("Add", Span::call_site()),
@ -1074,6 +1113,8 @@ mod tests {
path: None, path: None,
skip_impl: false, skip_impl: false,
properties_string: None, properties_string: None,
cfg: None,
shader_node: None,
}, },
fn_name: Ident::new("load_image", Span::call_site()), fn_name: Ident::new("load_image", Span::call_site()),
struct_name: Ident::new("LoadImage", Span::call_site()), struct_name: Ident::new("LoadImage", Span::call_site()),
@ -1131,6 +1172,8 @@ mod tests {
path: None, path: None,
skip_impl: false, skip_impl: false,
properties_string: None, properties_string: None,
cfg: None,
shader_node: None,
}, },
fn_name: Ident::new("custom_node", Span::call_site()), fn_name: Ident::new("custom_node", Span::call_site()),
struct_name: Ident::new("CustomNode", Span::call_site()), struct_name: Ident::new("CustomNode", Span::call_site()),

View file

@ -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<Self> {
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))),
})
}
}