From 44277c963696c48cdc64e255dac72ee5ce1a925d Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Sun, 26 Jul 2026 20:58:11 +0000 Subject: [PATCH] Update node macro to new syntax --- .../libraries/core-types/src/context.rs | 225 ++++++- node-graph/libraries/core-types/src/gnode.rs | 120 +++- node-graph/libraries/core-types/src/gpoll.rs | 5 + node-graph/libraries/core-types/src/lib.rs | 1 + node-graph/libraries/core-types/src/ops.rs | 20 +- node-graph/libraries/core-types/src/wire.rs | 560 ++++++++++++++++++ node-graph/node-macro/src/codegen.rs | 347 +---------- node-graph/node-macro/src/gcodegen.rs | 462 +++++++++++++++ node-graph/node-macro/src/lib.rs | 1 + node-graph/node-macro/src/parsing.rs | 96 ++- .../src/shader_nodes/per_pixel_adjust.rs | 2 +- 11 files changed, 1492 insertions(+), 347 deletions(-) create mode 100644 node-graph/libraries/core-types/src/wire.rs create mode 100644 node-graph/node-macro/src/gcodegen.rs diff --git a/node-graph/libraries/core-types/src/context.rs b/node-graph/libraries/core-types/src/context.rs index 46a6b03e61..01a6574723 100644 --- a/node-graph/libraries/core-types/src/context.rs +++ b/node-graph/libraries/core-types/src/context.rs @@ -36,6 +36,9 @@ pub trait ExtractPosition { } pub trait ExtractIndex { fn try_index(&self) -> Option>; + fn innermost_index(&self) -> u64 { + self.try_index().and_then(|mut indices| indices.next()).unwrap_or(0) as u64 + } } pub trait ExtractVarArgs { // TODO: Consider returning a slice or something like that @@ -466,7 +469,8 @@ impl CloneVarArgs for Arc { // TYPES `Context` AND `OwnedContextImpl` // ====================================== -pub type Context<'a> = Option>; +pub type OwnedContext = Option>; +pub type Context<'a> = ContextImpl<'a>; type DynRef<'a> = &'a (dyn Any + Send + Sync); type DynBox = Box; @@ -655,9 +659,39 @@ pub struct PositionLink<'a> { pub type DynSlot<'a> = &'a (dyn AnyHash + Send + Sync); +#[derive(Clone, Copy)] +pub enum VarArgSlots<'a> { + Single(DynSlot<'a>), + Slice(&'a [DynSlot<'a>]), +} + +impl<'a> VarArgSlots<'a> { + pub fn get(&self, index: usize) -> Option> { + match self { + VarArgSlots::Single(slot) => (index == 0).then_some(*slot), + VarArgSlots::Slice(slots) => slots.get(index).copied(), + } + } + + pub fn len(&self) -> usize { + match self { + VarArgSlots::Single(_) => 1, + VarArgSlots::Slice(slots) => slots.len(), + } + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + pub fn iter(&self) -> impl Iterator> + '_ { + (0..self.len()).filter_map(move |index| self.get(index)) + } +} + #[derive(Clone, Copy)] pub struct VarArgLink<'a> { - pub args: &'a [DynSlot<'a>], + pub args: VarArgSlots<'a>, pub outer: Option<&'a VarArgLink<'a>>, } @@ -704,6 +738,17 @@ impl<'a> EvalScope<'a> { scope } + pub fn nullified(&self, keep: ContextFeatures) -> EvalScope<'a> { + let mut scope = EvalScope { + real_time: self.real_time.filter(|_| keep.contains(ContextFeatures::REAL_TIME)), + animation_time: self.animation_time.filter(|_| keep.contains(ContextFeatures::ANIMATION_TIME)), + pointer_position: self.pointer_position.filter(|_| keep.contains(ContextFeatures::POINTER_POSITION)), + ..*self + }; + scope.hash = scope.compute_hash(None); + scope + } + fn compute_hash(&self, retain: Option<&[SourceId]>) -> u64 { let mut hasher = std::hash::DefaultHasher::new(); self.real_time.map(f64::to_bits).hash(&mut hasher); @@ -735,6 +780,106 @@ pub trait ExtractArena { fn arena(&self) -> Self::ArenaRef; } +pub trait CtxFamily { + type Ctx<'s>: Ctx + DeriveCtx; +} + +pub type Derived<'s, C> = <::Family as CtxFamily>::Ctx<'s>; + +pub trait DeriveCtx { + type Family: CtxFamily; + fn derived(&self) -> Derived<'_, Self>; + fn index_head(&self) -> IndexLink<'_>; + fn scope(&self) -> &EvalScope<'_>; + fn position_head(&self) -> Option<&PositionLink<'_>>; + fn varargs_head(&self) -> Option<&VarArgLink<'_>>; + fn promoted<'s>(&'s self, spilled_head: &'s IndexLink<'s>, inner_index: u64) -> Derived<'s, Self>; + fn with_footprint<'s>(&'s self, footprint: &'s Footprint) -> Derived<'s, Self>; + fn with_varargs<'s>(&'s self, varargs: &'s VarArgLink<'s>) -> Derived<'s, Self>; + fn with_position<'s>(&'s self, position: &'s PositionLink<'s>) -> Derived<'s, Self>; + fn with_scope<'s>(&'s self, scope: &'s EvalScope<'s>) -> Derived<'s, Self>; + fn nullified<'s>(&'s self, keep: ContextFeatures, scope: &'s EvalScope<'s>) -> Derived<'s, Self>; + + fn modify_footprint(&self, modify: impl FnOnce(&mut Footprint)) -> ModifiedFootprint<'_, Self> + where + Self: ExtractFootprint + Sized, + { + let mut footprint = self.try_footprint().copied(); + if let Some(footprint) = &mut footprint { + modify(footprint); + } + ModifiedFootprint { ctx: self, footprint } + } + + fn push_vararg<'s>(&'s self, arg: DynSlot<'s>) -> VarArgScope<'s, Self> + where + Self: Sized, + { + VarArgScope { + ctx: self, + link: VarArgLink { + args: VarArgSlots::Single(arg), + outer: self.varargs_head(), + }, + } + } + + fn push_position(&self, position: DVec2) -> PositionScope<'_, Self> + where + Self: Sized, + { + PositionScope { + ctx: self, + link: PositionLink { + position, + outer: self.position_head(), + }, + } + } +} + +pub struct PositionScope<'c, C> { + ctx: &'c C, + link: PositionLink<'c>, +} + +impl PositionScope<'_, C> { + pub fn ctx(&self) -> Derived<'_, C> { + self.ctx.with_position(&self.link) + } +} + +pub struct VarArgScope<'c, C> { + ctx: &'c C, + link: VarArgLink<'c>, +} + +impl VarArgScope<'_, C> { + pub fn ctx(&self) -> Derived<'_, C> { + self.ctx.with_varargs(&self.link) + } +} + +pub struct ModifiedFootprint<'c, C> { + ctx: &'c C, + footprint: Option, +} + +impl ModifiedFootprint<'_, C> { + pub fn ctx(&self) -> Derived<'_, C> { + match &self.footprint { + Some(footprint) => self.ctx.with_footprint(footprint), + None => self.ctx.derived(), + } + } +} + +pub struct ContextImplFamily; + +impl CtxFamily for ContextImplFamily { + type Ctx<'s> = ContextImpl<'s>; +} + #[derive(Clone, Copy)] pub struct ContextImpl<'a> { index: IndexLink<'a>, @@ -791,6 +936,22 @@ impl<'a> ContextImpl<'a> { ContextImpl { position: Some(position), ..*self } } + pub fn nullified<'s>(&self, keep: ContextFeatures, scope: &'s EvalScope<'s>) -> ContextImpl<'s> + where + 'a: 's, + { + ContextImpl { + index: match keep.contains(ContextFeatures::INDEX) { + true => self.index, + false => IndexLink { index: 0, outer: None }, + }, + position: self.position.filter(|_| keep.contains(ContextFeatures::POSITION)), + varargs: self.varargs.filter(|_| keep.contains(ContextFeatures::VARARGS)), + footprint: self.footprint.filter(|_| keep.contains(ContextFeatures::FOOTPRINT)), + scope, + } + } + pub fn promoted<'s>(&self, spilled_head: &'s IndexLink<'s>, inner_index: u64) -> ContextImpl<'s> where 'a: 's, @@ -849,7 +1010,7 @@ impl ExtractVarArgs for ContextImpl<'_> { let mut remaining = index; loop { match link.args.get(remaining) { - Some(arg) => return Ok(*arg as DynRef<'_>), + Some(arg) => return Ok(arg as DynRef<'_>), None => { remaining -= link.args.len(); link = link.outer.ok_or(VarArgsResult::IndexOutOfBounds)?; @@ -867,7 +1028,7 @@ impl ExtractVarArgs for ContextImpl<'_> { let mut count = 0u64; let mut link = self.varargs; while let Some(current) = link { - for arg in current.args { + for arg in current.args.iter() { arg.dyn_hash(&mut *hasher); count += 1; } @@ -883,6 +1044,54 @@ impl<'a> ExtractArena for ContextImpl<'a> { } } +impl<'a> DeriveCtx for ContextImpl<'a> { + type Family = ContextImplFamily; + + fn derived(&self) -> ContextImpl<'_> { + *self + } + + fn index_head(&self) -> IndexLink<'_> { + self.index + } + + fn scope(&self) -> &EvalScope<'_> { + self.scope + } + + fn position_head(&self) -> Option<&PositionLink<'_>> { + self.position + } + + fn varargs_head(&self) -> Option<&VarArgLink<'_>> { + self.varargs + } + + fn promoted<'s>(&'s self, spilled_head: &'s IndexLink<'s>, inner_index: u64) -> ContextImpl<'s> { + ContextImpl::promoted(self, spilled_head, inner_index) + } + + fn with_footprint<'s>(&'s self, footprint: &'s Footprint) -> ContextImpl<'s> { + ContextImpl::with_footprint(self, footprint) + } + + fn with_varargs<'s>(&'s self, varargs: &'s VarArgLink<'s>) -> ContextImpl<'s> { + ContextImpl::with_varargs(self, varargs) + } + + fn with_position<'s>(&'s self, position: &'s PositionLink<'s>) -> ContextImpl<'s> { + ContextImpl::with_position(self, position) + } + + fn with_scope<'s>(&'s self, scope: &'s EvalScope<'s>) -> ContextImpl<'s> { + ContextImpl::with_scope(self, scope) + } + + fn nullified<'s>(&'s self, keep: ContextFeatures, scope: &'s EvalScope<'s>) -> ContextImpl<'s> { + ContextImpl::nullified(self, keep, scope) + } +} + impl graphene_hash::CacheHash for ContextImpl<'_> { fn cache_hash(&self, state: &mut H) { match self.footprint { @@ -1053,13 +1262,15 @@ mod context_impl_tests { let outer_value = 7u32; let outer_args: [DynSlot; 1] = [&outer_value]; - let outer_link = VarArgLink { args: &outer_args, outer: None }; + let outer_link = VarArgLink { + args: VarArgSlots::Slice(&outer_args), + outer: None, + }; let outer_ctx = root.with_varargs(&outer_link); let inner_value = String::from("inner"); - let inner_args: [DynSlot; 1] = [&inner_value]; let inner_link = VarArgLink { - args: &inner_args, + args: VarArgSlots::Single(&inner_value), outer: Some(&outer_link), }; let inner_ctx = root.with_varargs(&inner_link); diff --git a/node-graph/libraries/core-types/src/gnode.rs b/node-graph/libraries/core-types/src/gnode.rs index 4374d43863..7bf335b02a 100644 --- a/node-graph/libraries/core-types/src/gnode.rs +++ b/node-graph/libraries/core-types/src/gnode.rs @@ -1,5 +1,6 @@ use crate::context::InjectIndex; -use crate::gpoll::{Extent, Finality, GPoll, GraphError}; +use crate::gpoll::{Extent, Finality, GPoll, GraphError, Interrupt}; +use std::cell::Cell; use std::mem::MaybeUninit; use std::ops::Range; @@ -118,6 +119,123 @@ where } } +pub struct StatusCell { + finality: Cell, + error: Cell>, + no_partial: bool, +} + +impl Default for StatusCell { + fn default() -> Self { + Self::new() + } +} + +impl StatusCell { + pub fn new() -> Self { + Self { + finality: Cell::new(Finality::AllFinal), + error: Cell::new(None), + no_partial: false, + } + } + + pub fn no_partial() -> Self { + Self { + no_partial: true, + ..Self::new() + } + } + + pub fn eval_input>(&self, input_index: usize, node: &N, input: &Input) -> Result { + match node.eval(input) { + GPoll::Final(value) => Ok(value), + GPoll::Partial(_) if self.no_partial => Err(Interrupt::Pending), + GPoll::Partial(value) => { + self.finality.set(Finality::Partial); + Ok(value) + } + GPoll::Fallback(boxed) => { + let (value, error) = *boxed; + let first = self.error.take(); + self.error.set(first.or(Some(error.traced(input_index)))); + Ok(value) + } + GPoll::Pending => Err(Interrupt::Pending), + GPoll::Error(mut error) => { + error.trace.push(input_index); + Err(Interrupt::Error(error)) + } + } + } + + pub fn finish(self, value: T) -> GPoll { + match (self.error.take(), self.finality.get()) { + (Some(error), _) => GPoll::Fallback(Box::new((value, error))), + (None, Finality::AllFinal) => GPoll::Final(value), + (None, Finality::Partial) => GPoll::Partial(value), + } + } + + pub fn merge(self, poll: GPoll) -> GPoll { + match poll { + GPoll::Final(value) => self.finish(value), + GPoll::Partial(value) => match self.finish(value) { + GPoll::Final(value) => GPoll::Partial(value), + other => other, + }, + GPoll::Fallback(boxed) => { + let (value, error) = *boxed; + let first = self.error.take().unwrap_or(error); + GPoll::Fallback(Box::new((value, first))) + } + interrupted => interrupted, + } + } +} + +#[derive(Clone, Copy)] +pub struct LazyInput<'a, N> { + node: &'a N, + cell: &'a StatusCell, + input_index: usize, +} + +impl<'a, N> LazyInput<'a, N> { + pub fn new(node: &'a N, cell: &'a StatusCell, input_index: usize) -> Self { + Self { node, cell, input_index } + } + + pub fn eval(&self, ctx: &Input) -> Result + where + N: GNode, + { + self.cell.eval_input(self.input_index, self.node, ctx) + } +} + +impl<'a, Input, N> GNode for LazyInput<'a, N> +where + N: GNode, +{ + type Output = N::Output; + + fn eval(&self, input: &Input) -> GPoll { + self.node.eval(input) + } + + fn extent(&self, input: &Input) -> GPoll { + self.node.extent(input) + } + + fn eval_batch<'b>(&self, input: &'b Input, range: Range, scratch: Option<&'b mut [MaybeUninit]>) -> BatchStatus<'b, Self::Output> + where + Input: InjectIndex + Copy, + { + self.node.eval_batch(input, range, scratch) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/node-graph/libraries/core-types/src/gpoll.rs b/node-graph/libraries/core-types/src/gpoll.rs index 3d670647db..ead0a47034 100644 --- a/node-graph/libraries/core-types/src/gpoll.rs +++ b/node-graph/libraries/core-types/src/gpoll.rs @@ -23,6 +23,11 @@ impl GraphError { trace: Vec::new(), } } + + pub fn traced(mut self, input_index: usize) -> Self { + self.trace.push(input_index); + self + } } #[derive(Clone, Debug, PartialEq)] diff --git a/node-graph/libraries/core-types/src/lib.rs b/node-graph/libraries/core-types/src/lib.rs index f7a9b25bab..3036ddcfb8 100644 --- a/node-graph/libraries/core-types/src/lib.rs +++ b/node-graph/libraries/core-types/src/lib.rs @@ -18,6 +18,7 @@ pub mod render_complexity; pub mod transform; pub mod uuid; pub mod value; +pub mod wire; pub use crate as core_types; pub use blending::*; diff --git a/node-graph/libraries/core-types/src/ops.rs b/node-graph/libraries/core-types/src/ops.rs index 9d812c9576..a063ef4b31 100644 --- a/node-graph/libraries/core-types/src/ops.rs +++ b/node-graph/libraries/core-types/src/ops.rs @@ -44,13 +44,13 @@ impl<'i, N: for<'a> Node<'a, I> + Copy, I: 'i> Copy for TypeNode: Sized { /// Converts this type into the (usually inferred) output type. #[must_use] - fn convert(self, footprint: Footprint, converter: C) -> impl Future + Send; + fn convert(self, footprint: Footprint, converter: C) -> T; } impl Convert for T { /// Converts this type into a `String` using its `ToString` implementation. #[inline] - async fn convert(self, _: Footprint, _converter: ()) -> String { + fn convert(self, _: Footprint, _converter: ()) -> String { self.to_string() } } @@ -60,7 +60,7 @@ pub trait ListConvert { } impl + Send> Convert, ()> for List { - async fn convert(self, _: Footprint, _: ()) -> List { + fn convert(self, _: Footprint, _: ()) -> List { let list: List = self .into_iter() .map(|row| { @@ -76,7 +76,7 @@ impl + Send> Convert, ()> for List { /// from any `List` express their signature as `AttributeDyn` and avoid monomorphizing /// over `U`; the compiler inserts this convert to bridge concrete-typed graph wires to the dyn input. impl Convert for List { - async fn convert(self, _: Footprint, _: ()) -> AttributeDyn { + fn convert(self, _: Footprint, _: ()) -> AttributeDyn { let values: Vec = self.into_iter().map(|row| row.into_element()).collect(); AttributeDyn(Box::new(Attribute(values))) } @@ -86,7 +86,7 @@ impl Convert for T { - async fn convert(self, _: Footprint, _: ()) -> AttributeValueDyn { + fn convert(self, _: Footprint, _: ()) -> AttributeValueDyn { AttributeValueDyn(Box::new(self)) } } @@ -95,13 +95,13 @@ impl Convert for List { - async fn convert(self, _: Footprint, _: ()) -> ListDyn { + fn convert(self, _: Footprint, _: ()) -> ListDyn { self.into() } } impl Convert for DVec2 { - async fn convert(self, _: Footprint, _: ()) -> DVec2 { + fn convert(self, _: Footprint, _: ()) -> DVec2 { self } } @@ -115,7 +115,7 @@ pub trait FromAnchorPosition { // Converts a position into a vector path composed of a single anchor point impl Convert, ()> for DVec2 { - async fn convert(self, _: Footprint, _: ()) -> List { + fn convert(self, _: Footprint, _: ()) -> List { List::new_from_item(Item::new_from_element(T::from_anchor_position(self))) } } @@ -124,7 +124,7 @@ impl Convert, ()> for DVec2 { macro_rules! impl_convert { ($from:ty, $to:ty) => { impl Convert<$to, ()> for $from { - async fn convert(self, _: Footprint, _: ()) -> $to { + fn convert(self, _: Footprint, _: ()) -> $to { self as $to } } @@ -146,7 +146,7 @@ macro_rules! impl_convert { impl_convert!(usize, $to); impl Convert for $to { - async fn convert(self, _: Footprint, _: ()) -> DVec2 { + fn convert(self, _: Footprint, _: ()) -> DVec2 { DVec2::splat(self as f64) } } diff --git a/node-graph/libraries/core-types/src/wire.rs b/node-graph/libraries/core-types/src/wire.rs new file mode 100644 index 0000000000..e97f38b246 --- /dev/null +++ b/node-graph/libraries/core-types/src/wire.rs @@ -0,0 +1,560 @@ +use crate::Type; +use crate::arena::{Arena, ArenaCell}; +use crate::concrete; +use crate::context::{ContextImpl, Ctx, ExtractArena}; +use crate::frame_table::{FrameTable, Lookup}; +use crate::gnode::GNode; +use crate::gpoll::{Extent, Finality, GPoll}; +use graphene_hash::CacheHash; +use std::any::Any; +use std::hash::Hasher; +use std::sync::Mutex; + +pub type ErasedGNode = dyn for<'c> GNode, Output = T>; +pub type ErasedLendGNode = dyn for<'c> GNode, Output = &'c T>; + +pub fn cache_key(ctx: &C) -> u64 { + let mut hasher = std::hash::DefaultHasher::new(); + ctx.cache_hash(&mut hasher); + hasher.finish() +} + +#[derive(Debug, PartialEq)] +pub enum WireError { + Arity { expected: usize, got: usize }, + Type { expected: Type, found: Type }, + MissingCapability { ty: Type }, +} + +#[derive(Clone, Copy, Default)] +pub struct WireCapabilities { + pub memoize: Option Result>, + pub lend: Option Result>, +} + +fn memoize_edge(edge: EdgeHandle) -> Result { + let content = edge.downcast::()?; + Ok(EdgeHandle::new(Box::new(MemoizeNode::new(content)) as Box>)) +} + +fn lend_edge(edge: EdgeHandle) -> Result { + let content = edge.downcast::()?; + Ok(EdgeHandle::new_ref(Box::new(FrameMemoNode::new(content)) as Box>)) +} + +pub struct EdgeHandle { + node: Box, + ty: Type, + capabilities: WireCapabilities, +} + +impl std::fmt::Debug for EdgeHandle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("EdgeHandle").field("ty", &self.ty).finish_non_exhaustive() + } +} + +impl EdgeHandle { + pub fn new(node: Box>) -> Self { + Self::new_erased( + node, + concrete!(T), + WireCapabilities { + memoize: Some(memoize_edge::), + lend: Some(lend_edge::), + }, + ) + } + + pub fn new_ref(node: Box>) -> Self { + Self::new_erased(node, Type::Ref(Box::new(concrete!(T))), WireCapabilities::default()) + } + + pub fn new_erased(node: Box, ty: Type, capabilities: WireCapabilities) -> Self + where + Box: Any, + { + Self { + node: Box::new(node), + ty, + capabilities, + } + } + + pub fn wire_type(&self) -> &Type { + &self.ty + } + + pub fn memoized(self) -> Result { + match self.capabilities.memoize { + Some(wrap) => wrap(self), + None => Err(WireError::MissingCapability { ty: self.ty }), + } + } + + pub fn lent(self) -> Result { + match self.capabilities.lend { + Some(wrap) => wrap(self), + None => Err(WireError::MissingCapability { ty: self.ty }), + } + } + + pub fn downcast(self) -> Result>, WireError> { + self.downcast_erased(concrete!(T)) + } + + pub fn downcast_lend(self) -> Result>, WireError> { + self.downcast_erased(Type::Ref(Box::new(concrete!(T)))) + } + + pub fn downcast_erased(self, expected: Type) -> Result, WireError> + where + Box: Any, + { + let found = self.ty; + self.node.downcast::>().map(|node| *node).map_err(|_| WireError::Type { expected, found }) + } +} + +pub struct NodeIoRecord { + pub inputs: Vec, + pub output: Type, +} + +pub struct RegistryEntry { + pub io: NodeIoRecord, + pub wire: fn(Vec) -> Result, +} + +pub fn resolve_and_wire(entry: &RegistryEntry, inputs: Vec) -> Result { + if inputs.len() != entry.io.inputs.len() { + return Err(WireError::Arity { + expected: entry.io.inputs.len(), + got: inputs.len(), + }); + } + for (handle, expected) in inputs.iter().zip(&entry.io.inputs) { + if handle.wire_type() != expected { + return Err(WireError::Type { + expected: expected.clone(), + found: handle.wire_type().clone(), + }); + } + } + (entry.wire)(inputs) +} + +pub struct MemoizeNode { + cache: Mutex>, + content: NodeContent, +} + +impl MemoizeNode { + pub fn new(content: NodeContent) -> Self { + Self { + cache: Mutex::new(None), + content, + } + } +} + +impl GNode for MemoizeNode +where + T: Clone, + Input: Ctx + CacheHash, + NodeContent: GNode, +{ + type Output = T; + + fn eval(&self, input: &Input) -> GPoll { + let key = cache_key(input); + if let Some((hash, value, finality)) = self.cache.lock().unwrap().as_ref() { + if *hash == key { + return match finality { + Finality::AllFinal => GPoll::Final(value.clone()), + Finality::Partial => GPoll::Partial(value.clone()), + }; + } + } + let result = self.content.eval(input); + match &result { + GPoll::Final(value) => *self.cache.lock().unwrap() = Some((key, value.clone(), Finality::AllFinal)), + GPoll::Partial(value) => *self.cache.lock().unwrap() = Some((key, value.clone(), Finality::Partial)), + GPoll::Pending | GPoll::Fallback(_) | GPoll::Error(_) => {} + } + result + } + + fn extent(&self, input: &Input) -> GPoll { + self.content.extent(input) + } +} + +pub struct FrameMemoNode { + cell: ArenaCell>, + content: NodeContent, +} + +impl FrameMemoNode { + pub fn new(content: NodeContent) -> Self { + Self { + cell: ArenaCell::new(), + content, + } + } +} + +impl<'e, T, Input, NodeContent> GNode for FrameMemoNode +where + T: Clone + 'static, + Input: Ctx + CacheHash + ExtractArena, + NodeContent: GNode, +{ + type Output = &'e T; + + fn eval(&self, input: &Input) -> GPoll<&'e T> { + let arena = input.arena(); + let table = match self.cell.load(arena) { + Some(table) => table, + None => match arena.alloc(FrameTable::new()) { + Some((table, weak)) => { + self.cell.store(weak); + table + } + None => return park(arena, self.content.eval(input)), + }, + }; + match table.lookup(cache_key(input)) { + Lookup::Hit(Finality::AllFinal, value) => GPoll::Final(value), + Lookup::Hit(Finality::Partial, value) => GPoll::Partial(value), + Lookup::Vacant(slot) => match self.content.eval(input) { + GPoll::Final(value) => GPoll::Final(slot.publish(value, Finality::AllFinal)), + GPoll::Partial(value) => GPoll::Partial(slot.publish(value, Finality::Partial)), + unpublishable => { + slot.release(); + park(arena, unpublishable) + } + }, + Lookup::Full => park(arena, self.content.eval(input)), + } + } + + fn extent(&self, input: &Input) -> GPoll { + self.content.extent(input) + } +} + +pub fn park<'e, T>(arena: &'e Arena, result: GPoll) -> GPoll<&'e T> { + match result { + GPoll::Final(value) => match arena.alloc(value) { + Some((parked, _)) => GPoll::Final(parked), + None => GPoll::arena_exhausted(), + }, + GPoll::Partial(value) => match arena.alloc(value) { + Some((parked, _)) => GPoll::Partial(parked), + None => GPoll::arena_exhausted(), + }, + GPoll::Fallback(boxed) => { + let (value, error) = *boxed; + match arena.alloc(value) { + Some((parked, _)) => GPoll::Fallback(Box::new((parked, error))), + None => GPoll::arena_exhausted(), + } + } + GPoll::Pending => GPoll::Pending, + GPoll::Error(error) => GPoll::Error(error), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::context::EvalScope; + use crate::SourceId; + use std::sync::atomic::{AtomicU32, Ordering}; + + struct CountingNode(AtomicU32); + + impl GNode for CountingNode { + type Output = u32; + + fn eval(&self, _input: &Input) -> GPoll { + GPoll::Final(self.0.fetch_add(1, Ordering::Relaxed) + 1) + } + } + + struct ValueNode(T); + + impl GNode for ValueNode { + type Output = T; + + fn eval(&self, _input: &Input) -> GPoll { + GPoll::Final(self.0.clone()) + } + } + + fn scope_fixture<'a>(generations: &'a [(SourceId, u64)], arena: &'a Arena) -> EvalScope<'a> { + EvalScope::new(Some(0.5), None, None, generations, arena) + } + + #[test] + fn memo_capability_wraps_edges_type_blind() { + let arena = Arena::new(1024); + let generations = []; + let scope = scope_fixture(&generations, &arena); + let ctx = ContextImpl::root(&scope); + + let edge = EdgeHandle::new(Box::new(CountingNode(AtomicU32::new(0))) as Box>); + let memoized = edge.memoized().unwrap().downcast::().unwrap(); + + assert_eq!(memoized.eval(&ctx), GPoll::Final(1)); + assert_eq!(memoized.eval(&ctx), GPoll::Final(1)); + } + + #[test] + fn memo_invalidates_on_generation_bump() { + let arena = Arena::new(1024); + let source: SourceId = 7; + let before = [(source, 1)]; + let after = [(source, 2)]; + let scope_before = scope_fixture(&before, &arena); + let scope_after = scope_fixture(&after, &arena); + + let edge = EdgeHandle::new(Box::new(CountingNode(AtomicU32::new(0))) as Box>); + let memoized = edge.memoized().unwrap().downcast::().unwrap(); + + assert_eq!(memoized.eval(&ContextImpl::root(&scope_before)), GPoll::Final(1)); + assert_eq!(memoized.eval(&ContextImpl::root(&scope_before)), GPoll::Final(1)); + assert_eq!(memoized.eval(&ContextImpl::root(&scope_after)), GPoll::Final(2)); + } + + #[test] + fn memoized_edges_stack_and_rewire() { + let arena = Arena::new(1024); + let generations = []; + let scope = scope_fixture(&generations, &arena); + let ctx = ContextImpl::root(&scope); + + let edge = EdgeHandle::new(Box::new(CountingNode(AtomicU32::new(0))) as Box>); + let stacked = edge.memoized().unwrap().memoized().unwrap().downcast::().unwrap(); + + assert_eq!(stacked.eval(&ctx), GPoll::Final(1)); + assert_eq!(stacked.eval(&ctx), GPoll::Final(1)); + } + + #[test] + fn lend_capability_turns_an_owned_edge_into_a_lending_edge() { + let arena = Arena::new(4096); + let generations = []; + let scope = scope_fixture(&generations, &arena); + let ctx = ContextImpl::root(&scope); + + let edge = EdgeHandle::new(Box::new(ValueNode("lent out".to_string())) as Box>); + let lending = edge.lent().unwrap(); + assert_eq!(*lending.wire_type(), Type::Ref(Box::new(concrete!(String)))); + + let node = lending.downcast_lend::().unwrap(); + let GPoll::Final(first) = node.eval(&ctx) else { + panic!("lend must fill the frame table and lend"); + }; + let GPoll::Final(second) = node.eval(&ctx) else { + panic!("second eval must lend the published value"); + }; + assert_eq!(first, "lent out"); + assert!(std::ptr::eq(first, second)); + } + + #[test] + fn ref_edges_report_missing_capabilities() { + let edge = EdgeHandle::new(Box::new(ValueNode(5u32)) as Box>); + let lending = edge.lent().unwrap(); + + match lending.memoized() { + Err(WireError::MissingCapability { ty }) => assert_eq!(ty, Type::Ref(Box::new(concrete!(u32)))), + other => panic!("expected missing capability, got {:?}", other.map(|handle| handle.ty)), + } + } + + #[test] + fn borrow_carrying_value_types_wire_through_the_general_constructor() { + struct SplitBorrow<'c>(&'c str, usize); + + struct SplitNode { + content: Node0, + } + + impl<'e, Input, Node0> GNode for SplitNode + where + Input: Ctx, + Node0: GNode, + { + type Output = SplitBorrow<'e>; + + fn eval(&self, input: &Input) -> GPoll> { + self.content.eval(input).map(|value| SplitBorrow(value, value.len())) + } + } + + type ErasedSplitEdge = dyn for<'c> GNode, Output = SplitBorrow<'c>>; + + let arena = Arena::new(4096); + let generations = []; + let scope = scope_fixture(&generations, &arena); + let ctx = ContextImpl::root(&scope); + + let lending = EdgeHandle::new(Box::new(ValueNode("held".to_string())) as Box>).lent().unwrap(); + let upstream = lending.downcast_lend::().unwrap(); + let node: Box = Box::new(SplitNode { content: upstream }); + let handle = EdgeHandle::new_erased(node, concrete!(SplitBorrow<'static>), WireCapabilities::default()); + assert_eq!(*handle.wire_type(), concrete!(SplitBorrow<'static>)); + + let wired = handle.downcast_erased::(concrete!(SplitBorrow<'static>)).unwrap(); + let GPoll::Final(split) = wired.eval(&ctx) else { + panic!("borrow-carrying output must eval through the erased edge"); + }; + assert_eq!(split.0, "held"); + assert_eq!(split.1, 4); + } + + #[test] + fn derive_ctx_repeat_pushes_index_levels_through_the_erased_edge() { + use crate::context::{Derived, DeriveCtx, ExtractIndex}; + + struct RepeatNode { + content: Node0, + } + + impl GNode for RepeatNode + where + C: Ctx + DeriveCtx, + Node0: for<'x> GNode, Output = T>, + { + type Output = Vec; + + fn eval(&self, input: &C) -> GPoll> { + let spilled = input.index_head(); + let mut result = Vec::new(); + for index in 0..3 { + let derived = input.promoted(&spilled, index); + match self.content.eval(&derived) { + GPoll::Final(value) => result.push(value), + other => return other.map(|_| Vec::new()), + } + } + GPoll::Final(result) + } + } + + struct LevelsNode; + + impl GNode for LevelsNode { + type Output = Vec; + + fn eval(&self, input: &Input) -> GPoll> { + GPoll::Final(input.try_index().map(|levels| levels.collect()).unwrap_or_default()) + } + } + + let arena = Arena::new(1024); + let generations = []; + let scope = scope_fixture(&generations, &arena); + let ctx = ContextImpl::root(&scope); + + let nested = RepeatNode { + content: RepeatNode { content: LevelsNode }, + }; + let erased: Box>>>> = Box::new(nested); + + let GPoll::Final(outer) = erased.eval(&ctx) else { + panic!("nested repeat must evaluate"); + }; + assert_eq!(outer.len(), 3); + assert_eq!(outer[2][1], vec![1, 2, 0]); + assert_eq!(outer[0][0], vec![0, 0, 0]); + } + + #[test] + fn derive_ctx_footprint_replace_reaches_the_content() { + use crate::context::{Derived, DeriveCtx, ExtractFootprint}; + use crate::transform::Footprint; + + struct ShiftFootprintNode { + content: Node0, + } + + impl GNode for ShiftFootprintNode + where + C: Ctx + DeriveCtx + ExtractFootprint, + Node0: for<'x> GNode, Output = T>, + { + type Output = T; + + fn eval(&self, input: &C) -> GPoll { + let mut footprint = input.try_footprint().copied().unwrap_or(Footprint::DEFAULT); + footprint.resolution.x += 7; + let derived = input.with_footprint(&footprint); + self.content.eval(&derived) + } + } + + struct ResolutionNode; + + impl GNode for ResolutionNode { + type Output = u32; + + fn eval(&self, input: &Input) -> GPoll { + GPoll::Final(input.try_footprint().map(|footprint| footprint.resolution.x).unwrap_or(0)) + } + } + + let arena = Arena::new(1024); + let generations = []; + let scope = scope_fixture(&generations, &arena); + let ctx = ContextImpl::root(&scope); + + let graph: Box> = Box::new(ShiftFootprintNode { + content: ShiftFootprintNode { content: ResolutionNode }, + }); + assert_eq!(graph.eval(&ctx), GPoll::Final(Footprint::DEFAULT.resolution.x + 14)); + } + + #[test] + fn resolve_and_wire_checks_arity_and_types() { + fn wire_strlen(args: Vec) -> Result { + let mut args = args.into_iter(); + let value = args.next().ok_or(WireError::Arity { expected: 1, got: 0 })?.downcast::()?; + drop(value); + Ok(EdgeHandle::new(Box::new(ValueNode(0u32)) as Box>)) + } + let entry = RegistryEntry { + io: NodeIoRecord { + inputs: vec![concrete!(String)], + output: concrete!(u32), + }, + wire: wire_strlen, + }; + + let owned = EdgeHandle::new(Box::new(ValueNode("typed".to_string())) as Box>); + assert!(resolve_and_wire(&entry, vec![owned]).is_ok()); + + assert_eq!(resolve_and_wire(&entry, vec![]).unwrap_err(), WireError::Arity { expected: 1, got: 0 }); + + let mistyped = EdgeHandle::new(Box::new(ValueNode(1.0f64)) as Box>); + assert_eq!( + resolve_and_wire(&entry, vec![mistyped]).unwrap_err(), + WireError::Type { + expected: concrete!(String), + found: concrete!(f64), + } + ); + + let lent = EdgeHandle::new(Box::new(ValueNode("typed".to_string())) as Box>).lent().unwrap(); + assert_eq!( + resolve_and_wire(&entry, vec![lent]).unwrap_err(), + WireError::Type { + expected: concrete!(String), + found: Type::Ref(Box::new(concrete!(String))), + } + ); + } +} diff --git a/node-graph/node-macro/src/codegen.rs b/node-graph/node-macro/src/codegen.rs index 3df1e855f4..da3bc8cbe5 100644 --- a/node-graph/node-macro/src/codegen.rs +++ b/node-graph/node-macro/src/codegen.rs @@ -1,28 +1,21 @@ use crate::parsing::*; use convert_case::{Case, Casing}; use proc_macro2::TokenStream as TokenStream2; -use quote::{ToTokens, format_ident, quote, quote_spanned}; +use quote::{ToTokens, format_ident, quote}; use std::sync::atomic::AtomicU64; use syn::punctuated::Punctuated; -use syn::spanned::Spanned; -use syn::token::Comma; -use syn::{Error, Ident, PatIdent, Token, WhereClause, WherePredicate, parse_quote}; +use syn::{Ident, PatIdent}; static NODE_ID: AtomicU64 = AtomicU64::new(0); pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn) -> syn::Result { let ParsedNodeFn { - vis, attributes, fn_name, struct_name, mod_name, fn_generics, - where_clause, input, - output_type, - is_async, fields, - body, description, .. } = parsed; @@ -80,13 +73,10 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn // Combined struct generic parameters with bounds for struct definition // struct MemoizeNode let struct_generic_params: Vec = data_field_generics.iter().map(|gp| quote!(#gp)).chain(node_generics.iter().map(|id| quote!(#id))).collect(); - let input_ident = &input.pat_ident; - let context_features = &input.context_features; // Regular field idents and names (for function parameters) let field_idents: Vec<_> = regular_fields.iter().map(|f| &f.pat_ident).collect(); - let field_names: Vec<_> = field_idents.iter().map(|pat_ident| &pat_ident.ident).collect(); let regular_field_names: Vec<_> = regular_fields.iter().map(|f| &f.pat_ident.ident).collect(); let data_field_names: Vec<_> = data_fields.iter().map(|f| &f.pat_ident.ident).collect(); @@ -121,33 +111,6 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let struct_fields = data_field_defs.chain(regular_field_defs); - let mut future_idents = Vec::new(); - - // Data fields get passed as references to the underlying function - let data_field_idents: Vec<_> = data_fields.iter().map(|f| &f.pat_ident).collect(); - let data_field_types: Vec<_> = data_fields - .iter() - .map(|field| match &field.ty { - ParsedFieldType::Regular(RegularParsedField { ty, .. }) => { - let ty = ty.clone(); - quote!(&#ty) - } - _ => unreachable!("Data fields must be Regular types, not Node types"), - }) - .collect(); - - // Regular fields have types passed to the function - let field_types: Vec<_> = regular_fields - .iter() - .map(|field| match &field.ty { - ParsedFieldType::Regular(RegularParsedField { ty, .. }) => ty.clone(), - ParsedFieldType::Node(NodeParsedField { output_type, input_type, .. }) => match parsed.is_async { - true => parse_quote!(&'n impl #core_types::Node<'n, #input_type, Output = impl core::future::Future>), - false => parse_quote!(&'n impl #core_types::Node<'n, #input_type, Output = #output_type>), - }, - }) - .collect(); - // Only regular fields have UI metadata (data fields are internal state) let widget_override: Vec<_> = regular_fields .iter() @@ -233,39 +196,6 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn .collect(); // Only eval regular fields (data fields are accessed directly as self.field_name) - let eval_args = regular_fields.iter().map(|field| { - let name = &field.pat_ident.ident; - match &field.ty { - ParsedFieldType::Regular { .. } => { - quote! { let #name = self.#name.eval(__input.clone()).await; } - } - ParsedFieldType::Node { .. } => { - quote! { let #name = &self.#name; } - } - } - }); - - // Only regular fields can have min/max constraints - let min_max_args = regular_fields.iter().map(|field| match &field.ty { - ParsedFieldType::Regular(RegularParsedField { number_hard_min, number_hard_max, .. }) => { - let name = &field.pat_ident.ident; - let mut tokens = quote!(); - if let Some(min) = number_hard_min { - tokens.extend(quote_spanned! {min.span()=> - let #name = #core_types::misc::Clampable::clamp_hard_min(#name, #min); - }); - } - - if let Some(max) = number_hard_max { - tokens.extend(quote_spanned! {max.span()=> - let #name = #core_types::misc::Clampable::clamp_hard_max(#name, #max); - }); - } - tokens - } - ParsedFieldType::Node { .. } => quote!(), - }); - let all_implementation_types = fields.iter().flat_map(|field| match &field.ty { ParsedFieldType::Regular(RegularParsedField { implementations, .. }) => implementations.iter().cloned().collect::>(), ParsedFieldType::Node(NodeParsedField { implementations, .. }) => implementations @@ -275,61 +205,6 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn }); let all_implementation_types = all_implementation_types.chain(input.implementations.iter().cloned()); - let input_type = &parsed.input.ty; - let mut clauses = Vec::new(); - let mut clampable_clauses = Vec::new(); - - for (field, name) in regular_fields.iter().zip(node_generics.iter()) { - clauses.push(match (&field.ty, *is_async) { - ( - ParsedFieldType::Regular(RegularParsedField { - ty, number_hard_min, number_hard_max, .. - }), - _, - ) => { - let all_lifetime_ty = substitute_lifetimes(ty.clone(), "all"); - let id = future_idents.len(); - let fut_ident = format_ident!("F{}", id); - future_idents.push(fut_ident.clone()); - - // Add Clampable bound if this field uses hard_min or hard_max - if number_hard_min.is_some() || number_hard_max.is_some() { - // The bound applies to the Output type of the future, which is #ty - clampable_clauses.push(quote!(#ty: #core_types::misc::Clampable)); - } - - quote!( - #fut_ident: core::future::Future + #core_types::WasmNotSend + 'n, - for<'all> #all_lifetime_ty: #core_types::WasmNotSend, - #name: #core_types::Node<'n, #input_type, Output = #fut_ident> + #core_types::WasmNotSync - ) - } - (ParsedFieldType::Node(NodeParsedField { input_type, output_type, .. }), true) => { - let id = future_idents.len(); - let fut_ident = format_ident!("F{}", id); - future_idents.push(fut_ident.clone()); - - quote!( - #fut_ident: core::future::Future + #core_types::WasmNotSend + 'n, - #name: #core_types::Node<'n, #input_type, Output = #fut_ident > + #core_types::WasmNotSync - ) - } - (ParsedFieldType::Node { .. }, false) => unreachable!("Found node which takes an impl Node<> input but is not async"), - }); - } - let where_clause = where_clause.clone().unwrap_or(WhereClause { - where_token: Token![where](output_type.span()), - predicates: Default::default(), - }); - - let mut struct_where_clause = where_clause.clone(); - let extra_where: Punctuated = parse_quote!( - #(#clauses,)* - #(#clampable_clauses,)* - #output_type: 'n, - ); - struct_where_clause.predicates.extend(extra_where); - // Only regular fields are parameters to new() let new_args = node_generics.iter().zip(regular_field_names.iter()).map(|(r#gen, name)| { quote! { #name: #r#gen } @@ -344,9 +219,6 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn }); let all_field_inits = data_inits.chain(regular_inits); - let async_keyword = is_async.then(|| quote!(async)); - let await_keyword = is_async.then(|| quote!(.await)); - // Data fields may not implement Copy, PartialEq, etc., so only derive Debug and Clone let struct_derives = if data_fields.is_empty() { quote!(#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]) @@ -354,34 +226,6 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn quote!(#[derive(Debug, Clone)]) }; - // Generate serialize method if serialize attribute is specified - let serialize_impl = if let Some(serialize_fn) = &parsed.attributes.serialize { - let data_field_refs = data_field_names.iter().map(|name| quote!(&self.#name)); - quote! { - fn serialize(&self) -> Option> { - #serialize_fn(#(#data_field_refs),*) - } - } - } else { - quote!() - }; - - let eval_impl = quote! { - type Output = #core_types::registry::DynFuture<'n, #output_type>; - #[inline] - fn eval(&'n self, __input: #input_type) -> Self::Output { - Box::pin(async move { - use #core_types::misc::Clampable; - - #(#eval_args)* - #(#min_max_args)* - self::#fn_name(__input #(, &self.#data_field_names)* #(, #regular_field_names)*) #await_keyword - }) - } - - #serialize_impl - }; - let identifier = format_ident!("{}_proto_ident", fn_name); let identifier_path = match parsed.attributes.path.as_ref() { Some(path) => { @@ -391,8 +235,18 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn None => quote!(std::module_path!()), }; - let register_node_impl = generate_register_node_impl(parsed, &field_names, &struct_name, &identifier)?; + let registry_name = format_ident!("__node_registry_{}_{}", NODE_ID.fetch_add(1, std::sync::atomic::Ordering::SeqCst), struct_name); + let register_node_impl = quote! { + #[cfg(target_family = "wasm")] + #[unsafe(no_mangle)] + extern "C" fn #registry_name() { + register_metadata(); + } + }; let import_name = format_ident!("_IMPORT_STUB_{}", mod_name.to_string().to_case(Case::UpperSnake)); + let gnode = crate::gcodegen::generate_gnode_code(crate_ident, parsed)?; + let gnode_in_mod = gnode.in_mod; + let gnode_top_level = gnode.top_level; let properties = &attributes.properties_string.as_ref().map(|value| quote!(Some(#value))).unwrap_or(quote!(None)); let memoize_flag = attributes.memoize; @@ -428,17 +282,7 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn Ok(quote! { #(#description_doc_attrs)* - #[inline] - #[allow(clippy::too_many_arguments)] - #vis #async_keyword fn #fn_name <'n, #(#fn_generics,)*> (#input_ident: #input_type #(, #data_field_idents: #data_field_types)* #(, #field_idents: #field_types)*) -> #output_type #where_clause #body - - #cfg - #[automatically_derived] - impl<'n, #(#fn_generics,)* #(#node_generics,)* #(#future_idents,)*> #core_types::Node<'n, #input_type> for #mod_name::#struct_name<#(#struct_type_params,)*> - #struct_where_clause - { - #eval_impl - } + #gnode_top_level #cfg const fn #identifier() -> #core_types::ProtoNodeIdentifier { @@ -458,10 +302,8 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn mod #mod_name { use super::*; use #core_types as gcore; - use gcore::{Node, NodeIOTypes, concrete, fn_type, fn_type_fut, future, ProtoNodeIdentifier, WasmNotSync, NodeIO, ContextFeature}; - use gcore::value::ClonedNode; - use gcore::ops::TypeNode; - use gcore::registry::{NodeMetadata, FieldMetadata, NODE_REGISTRY, NODE_METADATA, DynAnyNode, DowncastBothNode, DynFuture, TypeErasedBox, PanicNode, RegistryValueSource, RegistryWidgetOverride}; + use gcore::{ContextFeature, concrete}; + use gcore::registry::{NodeMetadata, FieldMetadata, NODE_METADATA, RegistryValueSource, RegistryWidgetOverride}; use gcore::ctor::ctor; // Use the types specified in the implementation @@ -484,6 +326,8 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn } } + #gnode_in_mod + #register_node_impl #[cfg_attr(not(target_family = "wasm"), ctor)] @@ -619,161 +463,10 @@ fn generate_phantom_data<'a>(fn_generics: impl Iterator Result { - // On native, `register_node` and `register_metadata` run automatically via `#[ctor]`. - // On Wasm, `ctor` isn't available, so this `extern "C"` fn is invoked from JS to register the same way. - // `skip_impl` nodes don't generate a `register_node`, so the shim calls only `register_metadata` for them. - let registry_name = format_ident!("__node_registry_{}_{}", NODE_ID.fetch_add(1, std::sync::atomic::Ordering::SeqCst), struct_name); - let register_node_call = if parsed.attributes.skip_impl { quote!() } else { quote!(register_node();) }; - let wasm_shim = quote! { - #[cfg(target_family = "wasm")] - #[unsafe(no_mangle)] - extern "C" fn #registry_name() { - #register_node_call - register_metadata(); - } - }; - - if parsed.attributes.skip_impl { - return Ok(wasm_shim); - } - - let mut constructors = Vec::new(); - let unit = parse_quote!(gcore::Context); - - let regular_fields: Vec<_> = parsed.fields.iter().filter(|f| !f.is_data_field).collect(); - - let parameter_types: Vec<_> = regular_fields - .iter() - .map(|field| { - match &field.ty { - ParsedFieldType::Regular(RegularParsedField { implementations, ty, .. }) => { - if !implementations.is_empty() { - implementations.iter().map(|ty| (&unit, ty)).collect() - } else { - vec![(&unit, ty)] - } - } - ParsedFieldType::Node(NodeParsedField { - implementations, - input_type, - output_type, - .. - }) => { - if !implementations.is_empty() { - implementations.iter().map(|impl_| (&impl_.input, &impl_.output)).collect() - } else { - vec![(input_type, output_type)] - } - } - } - .into_iter() - .map(|(input, out)| (substitute_lifetimes(input.clone(), "_"), substitute_lifetimes(out.clone(), "_"))) - .collect::>() - }) - .collect(); - - let max_implementations = parameter_types.iter().map(|x| x.len()).chain([parsed.input.implementations.len().max(1)]).max(); - - for i in 0..max_implementations.unwrap_or(0) { - let mut temp_constructors = Vec::new(); - let mut temp_node_io = Vec::new(); - let mut panic_node_types = Vec::new(); - - for (j, types) in parameter_types.iter().enumerate() { - let field_name = field_names[j]; - let (input_type, output_type) = &types[i.min(types.len() - 1)]; - - let node = matches!(regular_fields[j].ty, ParsedFieldType::Node { .. }); - - let downcast_node = quote!( - let #field_name: DowncastBothNode<#input_type, #output_type> = DowncastBothNode::new(args[#j].clone()); - ); - if node && !parsed.is_async { - return Err(Error::new_spanned(&parsed.fn_name, "Node needs to be async if you want to use lambda parameters")); - } - temp_constructors.push(downcast_node); - temp_node_io.push(quote!(fn_type_fut!(#input_type, #output_type, alias: #output_type))); - panic_node_types.push(quote!(#input_type, DynFuture<'static, #output_type>)); - } - let input_type = match parsed.input.implementations.is_empty() { - true => parsed.input.ty.clone(), - false => parsed.input.implementations[i.min(parsed.input.implementations.len() - 1)].clone(), - }; - constructors.push(quote!( - ( - |args| { - Box::pin(async move { - #(#temp_constructors;)* - let node = #struct_name::new(#(#field_names,)*); - // try polling futures - let any: DynAnyNode<#input_type, _, _> = DynAnyNode::new(node); - Box::new(any) as TypeErasedBox<'_> - }) - }, { - let node = #struct_name::new(#(PanicNode::<#panic_node_types>::new(),)*); - let params = vec![#(#temp_node_io,)*]; - let mut node_io = NodeIO::<'_, #input_type>::to_async_node_io(&node, params); - node_io - - } - ) - )); - } - Ok(quote! { - #[cfg_attr(not(target_family = "wasm"), ctor)] - fn register_node() { - let mut registry = NODE_REGISTRY.lock().unwrap(); - registry.insert( - #identifier(), - vec![ - #(#constructors,)* - ] - ); - } - - #wasm_shim - }) -} - use crate::crate_ident::CrateIdent; use crate::shader_nodes::{ShaderCodegen, ShaderTokens}; use syn::visit_mut::VisitMut; -use syn::{GenericArgument, Lifetime, Type}; - -struct LifetimeReplacer(&'static str); - -impl VisitMut for LifetimeReplacer { - fn visit_lifetime_mut(&mut self, lifetime: &mut Lifetime) { - lifetime.ident = Ident::new(self.0, lifetime.ident.span()); - } - - fn visit_type_mut(&mut self, ty: &mut Type) { - match ty { - Type::Reference(type_reference) => { - if let Some(lifetime) = &mut type_reference.lifetime { - self.visit_lifetime_mut(lifetime); - } - self.visit_type_mut(&mut type_reference.elem); - } - _ => syn::visit_mut::visit_type_mut(self, ty), - } - } - - fn visit_generic_argument_mut(&mut self, arg: &mut GenericArgument) { - if let GenericArgument::Lifetime(lifetime) = arg { - self.visit_lifetime_mut(lifetime); - } else { - syn::visit_mut::visit_generic_argument_mut(self, arg); - } - } -} - -#[must_use] -fn substitute_lifetimes(mut ty: Type, lifetime: &'static str) -> Type { - LifetimeReplacer(lifetime).visit_type_mut(&mut ty); - ty -} +use syn::{Lifetime, Type}; /// Get only the necessary generics. struct FilterUsedGenerics { @@ -856,7 +549,7 @@ impl FilterUsedGenerics { } /// Check if a type contains a reference to a specific identifier (e.g., a generic type parameter) -fn type_contains_ident(ty: &Type, ident: &Ident) -> bool { +pub(crate) fn type_contains_ident(ty: &Type, ident: &Ident) -> bool { struct IdentChecker<'a> { target: &'a Ident, found: bool, diff --git a/node-graph/node-macro/src/gcodegen.rs b/node-graph/node-macro/src/gcodegen.rs new file mode 100644 index 0000000000..1a953c4868 --- /dev/null +++ b/node-graph/node-macro/src/gcodegen.rs @@ -0,0 +1,462 @@ +use crate::crate_ident::CrateIdent; +use crate::parsing::*; +use proc_macro2::TokenStream as TokenStream2; +use quote::{format_ident, quote}; +use syn::visit::Visit; +use syn::{GenericArgument, GenericParam, Ident, Lifetime, PathArguments, Type, TypeParam, TypeParamBound}; + +pub(crate) struct GNodeTokens { + pub(crate) in_mod: TokenStream2, + pub(crate) top_level: TokenStream2, +} + +pub(crate) fn generate_gnode_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn) -> syn::Result { + let core_types = crate_ident.gcore()?; + + let ctx_param = context_param(parsed); + let ctx_ident = match ctx_param { + Some(ctx_param) => ctx_param.ident.clone(), + None => format_ident!("__Ctx"), + }; + let ctx_bounds: Vec = match ctx_param { + Some(ctx_param) => ctx_param + .bounds + .iter() + .filter_map(|bound| match bound { + TypeParamBound::Lifetime(_) => None, + bound => Some(desugar_extract_lifetime(bound, core_types)), + }) + .collect(), + None => vec![quote!(#core_types::Ctx)], + }; + + let derives = ctx_param.is_some_and(|ctx_param| { + ctx_param.bounds.iter().any(|bound| match bound { + TypeParamBound::Trait(trait_bound) => trait_bound.path.segments.last().is_some_and(|segment| segment.ident == "DeriveCtx"), + _ => false, + }) + }); + + let ctx_generic = match ctx_bounds.is_empty() { + true => quote!(#ctx_ident), + false => quote!(#ctx_ident: #(#ctx_bounds)+*), + }; + let mut generics: Vec = parsed + .fn_generics + .iter() + .map(|param| match param { + GenericParam::Type(type_param) if Some(&type_param.ident) == ctx_param.map(|ctx_param| &ctx_param.ident) => ctx_generic.clone(), + param => quote!(#param), + }) + .collect(); + if ctx_param.is_none() { + generics.push(ctx_generic); + } + + let fn_name = &parsed.fn_name; + let mod_name = format_ident!("_{}_mod", parsed.mod_name); + let struct_name = format_ident!("{}Node", parsed.struct_name); + let output_type = &parsed.output_type; + let trait_output = match kernel_kind(&parsed.output_type) { + KernelKind::Interrupt(inner) | KernelKind::Poll(inner) => inner, + KernelKind::Plain => parsed.output_type.clone(), + }; + let raw_lazy = matches!(kernel_kind(&parsed.output_type), KernelKind::Poll(_)); + let where_predicates: Vec = parsed.where_clause.iter().flat_map(|clause| clause.predicates.iter()).map(|predicate| quote!(#predicate)).collect(); + + let (data_fields, regular_fields): (Vec<_>, Vec<_>) = parsed.fields.iter().partition(|field| field.is_data_field); + + let data_field_generic_idents: Vec = parsed + .fn_generics + .iter() + .filter_map(|generic| match generic { + GenericParam::Type(type_param) => Some(type_param.ident.clone()), + _ => None, + }) + .filter(|ident| { + data_fields.iter().any(|field| match &field.ty { + ParsedFieldType::Regular(RegularParsedField { ty, .. }) => crate::codegen::type_contains_ident(ty, ident), + _ => false, + }) + }) + .collect(); + + let node_generics: Vec = regular_fields.iter().enumerate().map(|(index, _)| format_ident!("Node{}", index)).collect(); + let struct_type_params: Vec = data_field_generic_idents.iter().cloned().chain(node_generics.iter().cloned()).collect(); + + let data_names: Vec<&Ident> = data_fields.iter().map(|field| &field.pat_ident.ident).collect(); + let data_params = data_fields.iter().map(|field| { + let pat = &field.pat_ident; + let ParsedFieldType::Regular(RegularParsedField { ty, .. }) = &field.ty else { + unreachable!("data fields are regular types"); + }; + quote!(#pat: &#ty) + }); + + let lazy_bound = |output_type: &Type| match derives { + true => quote!(for<'__derived> #core_types::gnode::GNode<#core_types::context::Derived<'__derived, #ctx_ident>, Output = #output_type>), + false => quote!(#core_types::gnode::GNode<#ctx_ident, Output = #output_type>), + }; + + let kernel_params = regular_fields.iter().map(|field| { + let pat = &field.pat_ident; + match &field.ty { + ParsedFieldType::Regular(RegularParsedField { ty, .. }) => quote!(#pat: #ty), + ParsedFieldType::Node(NodeParsedField { output_type, .. }) if raw_lazy => { + let bound = lazy_bound(output_type); + quote!(#pat: &impl #bound) + } + ParsedFieldType::Node(NodeParsedField { output_type, .. }) => { + let bound = lazy_bound(output_type); + quote!(#pat: #core_types::gnode::LazyInput<'_, impl #bound>) + } + } + }); + + let node_bounds = regular_fields.iter().zip(&node_generics).map(|(field, node_generic)| match &field.ty { + ParsedFieldType::Regular(RegularParsedField { ty, .. }) => quote!(#node_generic: #core_types::gnode::GNode<#ctx_ident, Output = #ty>), + ParsedFieldType::Node(NodeParsedField { output_type, .. }) => { + let bound = lazy_bound(output_type); + quote!(#node_generic: #bound) + } + }); + + let clampable_bounds = regular_fields.iter().filter_map(|field| { + let ParsedFieldType::Regular(RegularParsedField { ty, number_hard_min, number_hard_max, .. }) = &field.ty else { + return None; + }; + (number_hard_min.is_some() || number_hard_max.is_some()).then(|| quote!(#ty: #core_types::misc::Clampable)) + }); + + let eval_values = regular_fields.iter().enumerate().map(|(index, field)| { + let name = &field.pat_ident.ident; + match &field.ty { + ParsedFieldType::Regular(_) => quote! { + let #name = match __cell.eval_input(#index, &self.#name, __input) { + Ok(value) => value, + Err(interrupt) => return interrupt.into(), + }; + }, + ParsedFieldType::Node(_) if raw_lazy => quote!(), + ParsedFieldType::Node(_) => quote! { + let #name = #core_types::gnode::LazyInput::new(&self.#name, &__cell, #index); + }, + } + }); + + let clamps = regular_fields.iter().filter_map(|field| { + let ParsedFieldType::Regular(RegularParsedField { number_hard_min, number_hard_max, .. }) = &field.ty else { + return None; + }; + let name = &field.pat_ident.ident; + let mut tokens = quote!(); + if let Some(min) = number_hard_min { + tokens.extend(quote!(let #name = #core_types::misc::Clampable::clamp_hard_min(#name, #min);)); + } + if let Some(max) = number_hard_max { + tokens.extend(quote!(let #name = #core_types::misc::Clampable::clamp_hard_max(#name, #max);)); + } + (!tokens.is_empty()).then_some(tokens) + }); + + let call_args = regular_fields.iter().map(|field| { + let name = &field.pat_ident.ident; + match &field.ty { + ParsedFieldType::Node(_) if raw_lazy => quote!(&self.#name), + _ => quote!(#name), + } + }); + + let value_field_names: Vec<&Ident> = regular_fields + .iter() + .filter(|field| matches!(field.ty, ParsedFieldType::Regular(_))) + .map(|field| &field.pat_ident.ident) + .collect(); + + let extent_impl = match &parsed.attributes.extent { + Some(path) => quote! { + fn extent(&self, __input: &#ctx_ident) -> #core_types::gpoll::GPoll<#core_types::gpoll::Extent> { + #path(self, __input) + } + }, + None if value_field_names.is_empty() => quote!(), + None => { + let first = value_field_names[0]; + let mut meet = quote!(self.#first.extent(__input)); + for name in &value_field_names[1..] { + meet = quote!(#core_types::gpoll::Extent::meet(#meet, self.#name.extent(__input))); + } + quote! { + fn extent(&self, __input: &#ctx_ident) -> #core_types::gpoll::GPoll<#core_types::gpoll::Extent> { + #meet + } + } + } + }; + + let batch_impl = match &parsed.attributes.batch { + Some(path) => quote! { + fn eval_batch<'__batch>( + &self, + __input: &'__batch #ctx_ident, + __range: ::std::ops::Range, + __scratch: Option<&'__batch mut [::std::mem::MaybeUninit]>, + ) -> #core_types::gnode::BatchStatus<'__batch, Self::Output> + where + #ctx_ident: #core_types::context::InjectIndex + Copy, + { + #path(self, __input, __range, __scratch) + } + }, + None => quote!(), + }; + + let ctx_pat = &parsed.input.pat_ident; + let fn_where = &parsed.where_clause; + let body = &parsed.body; + let vis = &parsed.vis; + let kernel = quote! { + #[allow(clippy::too_many_arguments)] + #vis fn #fn_name<#(#generics,)*>(#ctx_pat: &#ctx_ident #(, #data_params)* #(, #kernel_params)*) -> #output_type #fn_where #body + }; + let cell_constructor = match parsed.attributes.no_partial { + true => quote!(#core_types::gnode::StatusCell::no_partial()), + false => quote!(#core_types::gnode::StatusCell::new()), + }; + let kernel_call = quote!(self::#fn_name(__input #(, &self.#data_names)* #(, #call_args)*)); + let lift = match kernel_kind(&parsed.output_type) { + KernelKind::Interrupt(_) => quote! { + match #kernel_call { + Ok(value) => __cell.finish(value), + Err(interrupt) => interrupt.into(), + } + }, + KernelKind::Poll(_) => quote!(__cell.merge(#kernel_call)), + KernelKind::Plain => quote!(__cell.finish(#kernel_call)), + }; + + let wire = entries_tokens(parsed, &struct_name, &data_field_generic_idents, ®ular_fields); + let cfg = crate::shader_nodes::modify_cfg(&parsed.attributes); + let wire_reexport = match wire.is_empty() { + true => quote!(), + false => { + let entries_name = format_ident!("{}_entries", fn_name); + quote! { + #cfg + #[doc(hidden)] + pub use #mod_name::#entries_name; + } + } + }; + + let top_level = quote! { + #wire_reexport + + #cfg + #[automatically_derived] + impl<#(#generics,)* #(#node_generics,)*> #core_types::gnode::GNode<#ctx_ident> for #mod_name::#struct_name<#(#struct_type_params,)*> + where + #(#node_bounds,)* + #(#clampable_bounds,)* + #(#where_predicates,)* + { + type Output = #trait_output; + + fn eval(&self, __input: &#ctx_ident) -> #core_types::gpoll::GPoll { + let __cell = #cell_constructor; + #(#eval_values)* + #(#clamps)* + #lift + } + + #extent_impl + + #batch_impl + } + }; + + Ok(GNodeTokens { + in_mod: wire, + top_level: quote! { + #kernel + + #top_level + }, + }) +} + +enum KernelKind { + Plain, + Interrupt(Type), + Poll(Type), +} + +fn kernel_kind(output: &Type) -> KernelKind { + let plain = || KernelKind::Plain; + let Type::Path(path) = output else { return plain() }; + let Some(segment) = path.path.segments.last() else { return plain() }; + match segment.ident.to_string().as_str() { + "GPoll" => { + let PathArguments::AngleBracketed(args) = &segment.arguments else { return plain() }; + let inner = args.args.iter().find_map(|argument| match argument { + GenericArgument::Type(ty) => Some(ty.clone()), + _ => None, + }); + inner.map(KernelKind::Poll).unwrap_or_else(plain) + } + "Result" => { + let PathArguments::AngleBracketed(args) = &segment.arguments else { return plain() }; + let mut types = args.args.iter().filter_map(|argument| match argument { + GenericArgument::Type(ty) => Some(ty), + _ => None, + }); + let (Some(inner), Some(Type::Path(error_path))) = (types.next(), types.next()) else { + return plain(); + }; + match error_path.path.segments.last().is_some_and(|segment| segment.ident == "Interrupt") { + true => KernelKind::Interrupt(inner.clone()), + false => plain(), + } + } + _ => plain(), + } +} + +fn context_param<'a>(parsed: &'a ParsedNodeFn) -> Option<&'a TypeParam> { + let Type::Path(path) = &parsed.input.ty else { + return None; + }; + let ident = path.path.get_ident()?; + parsed.fn_generics.iter().find_map(|param| match param { + GenericParam::Type(type_param) if &type_param.ident == ident => Some(type_param), + _ => None, + }) +} + +fn type_disqualifies(ty: &Type) -> bool { + struct Disqualifier { + found: bool, + } + + impl<'ast> Visit<'ast> for Disqualifier { + fn visit_type_reference(&mut self, _: &'ast syn::TypeReference) { + self.found = true; + } + + fn visit_type_impl_trait(&mut self, _: &'ast syn::TypeImplTrait) { + self.found = true; + } + + fn visit_lifetime(&mut self, _: &'ast Lifetime) { + self.found = true; + } + } + + let mut visitor = Disqualifier { found: false }; + visitor.visit_type(ty); + visitor.found +} + +fn desugar_extract_lifetime(bound: &TypeParamBound, core_types: &TokenStream2) -> TokenStream2 { + let TypeParamBound::Trait(trait_bound) = bound else { + return quote!(#bound); + }; + let Some(segment) = trait_bound.path.segments.last() else { + return quote!(#bound); + }; + if segment.ident != "ExtractArena" { + return quote!(#bound); + } + let PathArguments::AngleBracketed(args) = &segment.arguments else { + return quote!(#bound); + }; + if args.args.len() != 1 { + return quote!(#bound); + } + let Some(GenericArgument::Lifetime(lifetime)) = args.args.first() else { + return quote!(#bound); + }; + quote!(#core_types::context::ExtractArena) +} + +fn entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, data_field_generic_idents: &[Ident], regular_fields: &[&ParsedField]) -> TokenStream2 { + if !data_field_generic_idents.is_empty() { + return quote!(); + } + let Some(rows) = implementation_rows(parsed, regular_fields) else { + return quote!(); + }; + let rows: Vec<&Vec> = rows.iter().filter(|row| row.iter().all(|ty| !type_disqualifies(ty))).collect(); + if rows.is_empty() { + return quote!(); + } + + let fn_name = &parsed.fn_name; + let entries_name = format_ident!("{}_entries", fn_name); + let arity = regular_fields.len(); + let names: Vec<&Ident> = regular_fields.iter().map(|field| &field.pat_ident.ident).collect(); + + let entries = rows.iter().map(|row| { + let types = row.iter(); + let boxed_types = row.iter().map(|ty| quote!(::std::boxed::Box>)); + let output = quote!(<#struct_name<#(#boxed_types),*> as gcore::gnode::GNode>>::Output); + let downcasts = names.iter().zip(row.iter()).map(|(name, ty)| { + quote!(let #name = inputs.next().unwrap().downcast::<#ty>()?;) + }); + quote! { + gcore::wire::RegistryEntry { + io: gcore::wire::NodeIoRecord { + inputs: vec![#(gcore::concrete!(#types)),*], + output: gcore::concrete!(#output), + }, + wire: |inputs| { + if inputs.len() != #arity { + return Err(gcore::wire::WireError::Arity { expected: #arity, got: inputs.len() }); + } + let mut inputs = inputs.into_iter(); + #(#downcasts)* + Ok(gcore::wire::EdgeHandle::new(::std::boxed::Box::new(#struct_name::new(#(#names),*)) as ::std::boxed::Box>)) + }, + } + } + }); + + quote! { + pub fn #entries_name() -> ::std::vec::Vec { + vec![#(#entries),*] + } + } +} + +fn implementation_rows(parsed: &ParsedNodeFn, regular_fields: &[&ParsedField]) -> Option>> { + let ctx_ident = context_param(parsed).map(|ctx| ctx.ident.clone()); + let open_generics: Vec<&Ident> = parsed + .fn_generics + .iter() + .filter_map(|param| match param { + GenericParam::Type(type_param) if Some(&type_param.ident) != ctx_ident.as_ref() => Some(&type_param.ident), + _ => None, + }) + .collect(); + + let candidates: Vec> = regular_fields + .iter() + .map(|field| match &field.ty { + ParsedFieldType::Regular(RegularParsedField { ty, implementations, .. }) => match implementations.is_empty() { + false => Some(implementations.iter().cloned().collect()), + true => open_generics.iter().all(|generic| !crate::codegen::type_contains_ident(ty, generic)).then(|| vec![ty.clone()]), + }, + ParsedFieldType::Node(NodeParsedField { output_type, implementations, .. }) => match implementations.is_empty() { + false => Some(implementations.iter().map(|implementation| implementation.output.clone()).collect()), + true => open_generics.iter().all(|generic| !crate::codegen::type_contains_ident(output_type, generic)).then(|| vec![output_type.clone()]), + }, + }) + .collect::>()?; + + let row_count = candidates.iter().map(|types| types.len()).max().unwrap_or(1).max(1); + Some( + (0..row_count) + .map(|row| candidates.iter().map(|types| types[row.min(types.len() - 1)].clone()).collect()) + .collect(), + ) +} diff --git a/node-graph/node-macro/src/lib.rs b/node-graph/node-macro/src/lib.rs index 35fe604a01..8302e512e1 100644 --- a/node-graph/node-macro/src/lib.rs +++ b/node-graph/node-macro/src/lib.rs @@ -7,6 +7,7 @@ mod buffer_struct; mod codegen; mod crate_ident; mod derive_choice_type; +mod gcodegen; mod parsing; mod shader_nodes; mod validation; diff --git a/node-graph/node-macro/src/parsing.rs b/node-graph/node-macro/src/parsing.rs index 09c1217aeb..61b8e95301 100644 --- a/node-graph/node-macro/src/parsing.rs +++ b/node-graph/node-macro/src/parsing.rs @@ -56,6 +56,14 @@ pub(crate) struct NodeFnAttributes { pub(crate) memoize: bool, /// Whether this node provides a scope pub(crate) inject_scope: bool, + /// Function producing a stand-in value while an async source node's real value is in flight + pub(crate) placeholder: Option, + /// Function overriding the generated `extent` method + pub(crate) extent: Option, + /// Function overriding the generated `eval_batch` method + pub(crate) batch: Option, + /// Whether partial upstream values are mapped to `Pending` instead of flowing into this node + pub(crate) no_partial: bool, } #[derive(Clone, Debug, Default)] @@ -311,6 +319,10 @@ impl Parse for NodeFnAttributes { let mut serialize = None; let mut memoize = false; let mut inject_scope = false; + let mut placeholder = None; + let mut extent = None; + let mut batch = None; + let mut no_partial = false; let content = input; // let content; @@ -453,13 +465,63 @@ impl Parse for NodeFnAttributes { } inject_scope = true; } + // Function producing a stand-in value for an async source node while the spawned future is in flight. + // The node reports `Partial` with the stand-in until the real value lands; without a placeholder it reports `Pending`. + // + // Example usage: + // #[node_macro::node(..., placeholder(empty_image), ...)] + "placeholder" => { + let meta = meta.require_list()?; + if placeholder.is_some() { + return Err(Error::new_spanned(meta, "Multiple 'placeholder' attributes are not allowed")); + } + let parsed_path: Path = meta + .parse_args() + .map_err(|_| Error::new_spanned(meta, "Expected a valid path for 'placeholder', e.g., placeholder(empty_image)"))?; + placeholder = Some(parsed_path); + } + // Function overriding the generated `extent` method, replacing the default meet over the node's inputs. + // + // Example usage: + // #[node_macro::node(..., extent(my_extent), ...)] + "extent" => { + let meta = meta.require_list()?; + if extent.is_some() { + return Err(Error::new_spanned(meta, "Multiple 'extent' attributes are not allowed")); + } + let parsed_path: Path = meta.parse_args().map_err(|_| Error::new_spanned(meta, "Expected a valid path for 'extent', e.g., extent(my_extent)"))?; + extent = Some(parsed_path); + } + // Function overriding the generated `eval_batch` method, replacing the trait's per-lane spec loop. + // + // Example usage: + // #[node_macro::node(..., batch(my_batch), ...)] + "batch" => { + let meta = meta.require_list()?; + if batch.is_some() { + return Err(Error::new_spanned(meta, "Multiple 'batch' attributes are not allowed")); + } + let parsed_path: Path = meta.parse_args().map_err(|_| Error::new_spanned(meta, "Expected a valid path for 'batch', e.g., batch(my_batch)"))?; + batch = Some(parsed_path); + } + // Instructs the generated eval to report `Pending` instead of passing partial upstream values into this node. + // + // Example usage: + // #[node_macro::node(..., no_partial, ...)] + "no_partial" => { + let path = meta.require_path_only()?; + if no_partial { + return Err(Error::new_spanned(path, "Multiple 'no_partial' attributes are not allowed")); + } + no_partial = true; + } _ => { return Err(Error::new_spanned( meta, indoc!( r#" Unsupported attribute in `node`. - Supported attributes are 'category', 'name', 'path', 'skip_impl', 'properties', 'cfg', 'shader_node', 'serialize', 'memoize', and 'inject_scope'. + Supported attributes are 'category', 'name', 'path', 'skip_impl', 'properties', 'cfg', 'shader_node', 'serialize', 'memoize', 'inject_scope', 'placeholder', 'extent', 'batch', and 'no_partial'. Example usage: #[node_macro::node(..., name("Test Node"), ...)] "# @@ -493,6 +555,10 @@ impl Parse for NodeFnAttributes { serialize, memoize, inject_scope, + placeholder, + extent, + batch, + no_partial, }) } } @@ -1082,6 +1148,10 @@ mod tests { serialize: None, memoize: false, inject_scope: false, + placeholder: None, + extent: None, + batch: None, + no_partial: false, }, fn_name: Ident::new("add", Span::call_site()), struct_name: Ident::new("Add", Span::call_site()), @@ -1152,6 +1222,10 @@ mod tests { serialize: None, memoize: false, inject_scope: false, + placeholder: None, + extent: None, + batch: None, + no_partial: false, }, fn_name: Ident::new("transform", Span::call_site()), struct_name: Ident::new("Transform", Span::call_site()), @@ -1236,6 +1310,10 @@ mod tests { serialize: None, memoize: false, inject_scope: false, + placeholder: None, + extent: None, + batch: None, + no_partial: false, }, fn_name: Ident::new("circle", Span::call_site()), struct_name: Ident::new("Circle", Span::call_site()), @@ -1302,6 +1380,10 @@ mod tests { serialize: None, memoize: false, inject_scope: false, + placeholder: None, + extent: None, + batch: None, + no_partial: false, }, fn_name: Ident::new("levels", Span::call_site()), struct_name: Ident::new("Levels", Span::call_site()), @@ -1380,6 +1462,10 @@ mod tests { serialize: None, memoize: false, inject_scope: false, + placeholder: None, + extent: None, + batch: None, + no_partial: false, }, fn_name: Ident::new("add", Span::call_site()), struct_name: Ident::new("Add", Span::call_site()), @@ -1461,6 +1547,10 @@ mod tests { serialize: None, memoize: false, inject_scope: false, + placeholder: None, + extent: None, + batch: None, + no_partial: false, }, fn_name: Ident::new("load_image", Span::call_site()), struct_name: Ident::new("LoadImage", Span::call_site()), @@ -1527,6 +1617,10 @@ mod tests { serialize: None, memoize: false, inject_scope: false, + placeholder: None, + extent: None, + batch: None, + no_partial: false, }, 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/per_pixel_adjust.rs b/node-graph/node-macro/src/shader_nodes/per_pixel_adjust.rs index 97f87fc4db..c849737c3e 100644 --- a/node-graph/node-macro/src/shader_nodes/per_pixel_adjust.rs +++ b/node-graph/node-macro/src/shader_nodes/per_pixel_adjust.rs @@ -146,7 +146,7 @@ impl PerPixelAdjustCodegen<'_> { ParamType::Uniform => quote!(uniform.#ident), }) .collect::>(); - let context = quote!(()); + let context = quote!(&()); let entry_point_mod = &self.entry_point_mod; let entry_point_name = &self.entry_point_name_ident;