From 62b7ccf40ce6d010e90fe7de500be25592724816 Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Mon, 3 Aug 2026 18:29:16 +0000 Subject: [PATCH] Support bare reference parameters and lending outputs in the node macro and make the clone node the clone-out adapter --- node-graph/graph-craft/src/proto.rs | 2 +- .../src/dynamic_executor.rs | 26 ++- .../interpreted-executor/src/node_registry.rs | 182 +++++++++--------- node-graph/node-macro/src/codegen.rs | 97 +++++++++- node-graph/node-macro/src/parsing.rs | 15 ++ .../src/shader_nodes/per_pixel_adjust.rs | 1 + node-graph/node-macro/src/validation.rs | 25 +++ node-graph/nodes/gcore/src/debug.rs | 4 +- node-graph/nodes/gcore/src/memo.rs | 72 +------ .../nodes/raster/src/image_color_palette.rs | 2 +- 10 files changed, 254 insertions(+), 172 deletions(-) diff --git a/node-graph/graph-craft/src/proto.rs b/node-graph/graph-craft/src/proto.rs index d4c999f4cf..1f1938e163 100644 --- a/node-graph/graph-craft/src/proto.rs +++ b/node-graph/graph-craft/src/proto.rs @@ -923,7 +923,7 @@ fn ref_adapter(proposed: &Type, wanted: &Type) -> Option { return None; }; match (proposed_output.as_ref(), wanted_output.as_ref()) { - (Type::Ref(inner), wanted_output @ Type::Concrete(_)) if valid_type(inner, wanted_output) => Some(ProtoNodeIdentifier::new("graphene_core::memo::CloneOutNode")), + (Type::Ref(inner), wanted_output @ Type::Concrete(_)) if valid_type(inner, wanted_output) => Some(ProtoNodeIdentifier::new("graphene_core::debug::CloneNode")), (proposed_output @ Type::Concrete(_), Type::Ref(inner)) if valid_type(proposed_output, inner) => Some(ProtoNodeIdentifier::new("graphene_core::memo::LendNode")), _ => None, } diff --git a/node-graph/interpreted-executor/src/dynamic_executor.rs b/node-graph/interpreted-executor/src/dynamic_executor.rs index e6d1dbae28..38505df10e 100644 --- a/node-graph/interpreted-executor/src/dynamic_executor.rs +++ b/node-graph/interpreted-executor/src/dynamic_executor.rs @@ -566,7 +566,7 @@ mod test { output: NodeId(1), nodes: vec![ (NodeId(0), string_value("lent")), - (NodeId(1), proto_node("graphene_core::memo::CloneOutNode", vec![NodeId(0)])), + (NodeId(1), proto_node("graphene_core::debug::CloneNode", vec![NodeId(0)])), ], }; @@ -574,6 +574,28 @@ mod test { assert_eq!((&executor).execute(()).unwrap(), GPoll::Final(TaggedValue::String("lent".to_string()))); } + #[test] + fn a_flipped_ref_parameter_gets_its_producer_lend_spliced() { + let raster_list = TaggedValue::from_type(&core_types::concrete!(graphene_std::list::List>)).unwrap(); + let network = ProtoNetwork { + inputs: vec![], + output: NodeId(2), + nodes: vec![ + (NodeId(0), ProtoNode::value(ConstructionArgs::Value(raster_list.into()), vec![])), + (NodeId(1), ProtoNode::value(ConstructionArgs::Value(TaggedValue::U32(4).into()), vec![])), + (NodeId(2), proto_node("raster_nodes::image_color_palette::ImageColorPaletteNode", vec![NodeId(0), NodeId(1)])), + ], + }; + + let executor = DynamicExecutor::new(network).unwrap(); + let arena = Arena::new(1 << 12).unwrap(); + let generations = []; + let scope = EvalScope::new(None, None, None, &generations, &arena); + let ctx = ContextImpl::root(&scope); + let result: Option>> = executor.tree().eval(NodeId(2), &ctx); + assert!(matches!(result, Some(GPoll::Final(_))), "the palette must evaluate through the spliced lend, got {result:?}"); + } + #[test] fn a_clone_out_adapter_is_spliced_between_a_lending_producer_and_an_owned_consumer() { let network = ProtoNetwork { @@ -583,7 +605,7 @@ mod test { (NodeId(0), string_value("memoized")), (NodeId(1), proto_node("graphene_core::memo::FrameMemoNode", vec![NodeId(0)])), (NodeId(2), proto_node("graphene_core::memo::FrameMemoNode", vec![NodeId(1)])), - (NodeId(3), proto_node("graphene_core::memo::CloneOutNode", vec![NodeId(2)])), + (NodeId(3), proto_node("graphene_core::debug::CloneNode", vec![NodeId(2)])), ], }; diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index 35673fa580..6d0b71a941 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -20,7 +20,7 @@ use graphene_std::transform::Footprint; use graphene_std::uuid::NodeId; use graphene_std::vector::Vector; use graphene_std::{Artboard, Context, Graphic, ProtoNodeIdentifier, SourceId, concrete, fn_type}; -use node_registry_macros::{async_node, clone_out_node, convert_node, frame_memo_node, into_node, lend_node}; +use node_registry_macros::{async_node, clone_node, convert_node, frame_memo_node, into_node, lend_node}; use std::collections::HashMap; #[cfg(feature = "gpu")] use wgpu_executor::WgpuExecutorHandle; @@ -299,265 +299,265 @@ fn node_registry() -> HashMap> { // REF ADAPTERS // ============ lend_node!(()), - clone_out_node!(()), + clone_node!(()), frame_memo_node!(()), lend_node!(RuntimeHandle), - clone_out_node!(RuntimeHandle), + clone_node!(RuntimeHandle), frame_memo_node!(RuntimeHandle), lend_node!(SourceId), - clone_out_node!(SourceId), + clone_node!(SourceId), frame_memo_node!(SourceId), lend_node!(bool), - clone_out_node!(bool), + clone_node!(bool), frame_memo_node!(bool), lend_node!(List), - clone_out_node!(List), + clone_node!(List), frame_memo_node!(List), lend_node!(List), - clone_out_node!(List), + clone_node!(List), frame_memo_node!(List), lend_node!(List), - clone_out_node!(List), + clone_node!(List), frame_memo_node!(List), lend_node!(List>), - clone_out_node!(List>), + clone_node!(List>), frame_memo_node!(List>), lend_node!(List), - clone_out_node!(List), + clone_node!(List), frame_memo_node!(List), lend_node!(Image), - clone_out_node!(Image), + clone_node!(Image), frame_memo_node!(Image), lend_node!(List), - clone_out_node!(List), + clone_node!(List), frame_memo_node!(List), lend_node!(List), - clone_out_node!(List), + clone_node!(List), frame_memo_node!(List), lend_node!(List), - clone_out_node!(List), + clone_node!(List), frame_memo_node!(List), lend_node!(List), - clone_out_node!(List), + clone_node!(List), frame_memo_node!(List), lend_node!(List), - clone_out_node!(List), + clone_node!(List), frame_memo_node!(List), lend_node!(List), - clone_out_node!(List), + clone_node!(List), frame_memo_node!(List), lend_node!(List), - clone_out_node!(List), + clone_node!(List), frame_memo_node!(List), lend_node!(List), - clone_out_node!(List), + clone_node!(List), frame_memo_node!(List), lend_node!(List), - clone_out_node!(List), + clone_node!(List), frame_memo_node!(List), lend_node!(List), - clone_out_node!(List), + clone_node!(List), frame_memo_node!(List), lend_node!(AttributeDyn), - clone_out_node!(AttributeDyn), + clone_node!(AttributeDyn), frame_memo_node!(AttributeDyn), lend_node!(AttributeValueDyn), - clone_out_node!(AttributeValueDyn), + clone_node!(AttributeValueDyn), frame_memo_node!(AttributeValueDyn), lend_node!(ListDyn), - clone_out_node!(ListDyn), + clone_node!(ListDyn), frame_memo_node!(ListDyn), #[cfg(target_family = "wasm")] lend_node!(CanvasHandle), #[cfg(target_family = "wasm")] - clone_out_node!(CanvasHandle), + clone_node!(CanvasHandle), #[cfg(target_family = "wasm")] frame_memo_node!(CanvasHandle), lend_node!(f64), - clone_out_node!(f64), + clone_node!(f64), frame_memo_node!(f64), lend_node!(f32), - clone_out_node!(f32), + clone_node!(f32), frame_memo_node!(f32), lend_node!(u32), - clone_out_node!(u32), + clone_node!(u32), frame_memo_node!(u32), lend_node!(u64), - clone_out_node!(u64), + clone_node!(u64), frame_memo_node!(u64), lend_node!(DVec2), - clone_out_node!(DVec2), + clone_node!(DVec2), frame_memo_node!(DVec2), lend_node!(String), - clone_out_node!(String), + clone_node!(String), frame_memo_node!(String), lend_node!(DAffine2), - clone_out_node!(DAffine2), + clone_node!(DAffine2), frame_memo_node!(DAffine2), lend_node!(Footprint), - clone_out_node!(Footprint), + clone_node!(Footprint), frame_memo_node!(Footprint), lend_node!(RenderOutput), - clone_out_node!(RenderOutput), + clone_node!(RenderOutput), frame_memo_node!(RenderOutput), lend_node!(std::sync::Arc), - clone_out_node!(std::sync::Arc), + clone_node!(std::sync::Arc), frame_memo_node!(std::sync::Arc), #[cfg(feature = "gpu")] lend_node!(List>), #[cfg(feature = "gpu")] - clone_out_node!(List>), + clone_node!(List>), #[cfg(feature = "gpu")] frame_memo_node!(List>), lend_node!(Option), - clone_out_node!(Option), + clone_node!(Option), frame_memo_node!(Option), lend_node!(Option), - clone_out_node!(Option), + clone_node!(Option), frame_memo_node!(Option), lend_node!(Graphic), - clone_out_node!(Graphic), + clone_node!(Graphic), frame_memo_node!(Graphic), lend_node!(glam::f32::Vec2), - clone_out_node!(glam::f32::Vec2), + clone_node!(glam::f32::Vec2), frame_memo_node!(glam::f32::Vec2), lend_node!(glam::f32::Affine2), - clone_out_node!(glam::f32::Affine2), + clone_node!(glam::f32::Affine2), frame_memo_node!(glam::f32::Affine2), lend_node!(graphene_std::vector::style::Stroke), - clone_out_node!(graphene_std::vector::style::Stroke), + clone_node!(graphene_std::vector::style::Stroke), frame_memo_node!(graphene_std::vector::style::Stroke), lend_node!(graphene_std::text::Font), - clone_out_node!(graphene_std::text::Font), + clone_node!(graphene_std::text::Font), frame_memo_node!(graphene_std::text::Font), lend_node!(List), - clone_out_node!(List), + clone_node!(List), frame_memo_node!(List), lend_node!(DocumentNode), - clone_out_node!(DocumentNode), + clone_node!(DocumentNode), frame_memo_node!(DocumentNode), lend_node!(graphene_std::ContextModification), - clone_out_node!(graphene_std::ContextModification), + clone_node!(graphene_std::ContextModification), frame_memo_node!(graphene_std::ContextModification), lend_node!(graphene_std::transform::Footprint), - clone_out_node!(graphene_std::transform::Footprint), + clone_node!(graphene_std::transform::Footprint), frame_memo_node!(graphene_std::transform::Footprint), lend_node!(Box), - clone_out_node!(Box), + clone_node!(Box), frame_memo_node!(Box), lend_node!(graphene_std::blending::BlendMode), - clone_out_node!(graphene_std::blending::BlendMode), + clone_node!(graphene_std::blending::BlendMode), frame_memo_node!(graphene_std::blending::BlendMode), lend_node!(graphene_std::raster::LuminanceCalculation), - clone_out_node!(graphene_std::raster::LuminanceCalculation), + clone_node!(graphene_std::raster::LuminanceCalculation), frame_memo_node!(graphene_std::raster::LuminanceCalculation), lend_node!(graphene_std::vector::QRCodeErrorCorrectionLevel), - clone_out_node!(graphene_std::vector::QRCodeErrorCorrectionLevel), + clone_node!(graphene_std::vector::QRCodeErrorCorrectionLevel), frame_memo_node!(graphene_std::vector::QRCodeErrorCorrectionLevel), lend_node!(graphene_std::extract_xy::XY), - clone_out_node!(graphene_std::extract_xy::XY), + clone_node!(graphene_std::extract_xy::XY), frame_memo_node!(graphene_std::extract_xy::XY), lend_node!(graphene_std::text_nodes::StringCapitalization), - clone_out_node!(graphene_std::text_nodes::StringCapitalization), + clone_node!(graphene_std::text_nodes::StringCapitalization), frame_memo_node!(graphene_std::text_nodes::StringCapitalization), lend_node!(graphene_std::raster::RedGreenBlue), - clone_out_node!(graphene_std::raster::RedGreenBlue), + clone_node!(graphene_std::raster::RedGreenBlue), frame_memo_node!(graphene_std::raster::RedGreenBlue), lend_node!(graphene_std::raster::RedGreenBlueAlpha), - clone_out_node!(graphene_std::raster::RedGreenBlueAlpha), + clone_node!(graphene_std::raster::RedGreenBlueAlpha), frame_memo_node!(graphene_std::raster::RedGreenBlueAlpha), lend_node!(graphene_std::animation::RealTimeMode), - clone_out_node!(graphene_std::animation::RealTimeMode), + clone_node!(graphene_std::animation::RealTimeMode), frame_memo_node!(graphene_std::animation::RealTimeMode), lend_node!(graphene_std::raster::NoiseType), - clone_out_node!(graphene_std::raster::NoiseType), + clone_node!(graphene_std::raster::NoiseType), frame_memo_node!(graphene_std::raster::NoiseType), lend_node!(graphene_std::raster::FractalType), - clone_out_node!(graphene_std::raster::FractalType), + clone_node!(graphene_std::raster::FractalType), frame_memo_node!(graphene_std::raster::FractalType), lend_node!(graphene_std::raster::CellularDistanceFunction), - clone_out_node!(graphene_std::raster::CellularDistanceFunction), + clone_node!(graphene_std::raster::CellularDistanceFunction), frame_memo_node!(graphene_std::raster::CellularDistanceFunction), lend_node!(graphene_std::raster::CellularReturnType), - clone_out_node!(graphene_std::raster::CellularReturnType), + clone_node!(graphene_std::raster::CellularReturnType), frame_memo_node!(graphene_std::raster::CellularReturnType), lend_node!(graphene_std::raster::DomainWarpType), - clone_out_node!(graphene_std::raster::DomainWarpType), + clone_node!(graphene_std::raster::DomainWarpType), frame_memo_node!(graphene_std::raster::DomainWarpType), lend_node!(graphene_std::raster::RelativeAbsolute), - clone_out_node!(graphene_std::raster::RelativeAbsolute), + clone_node!(graphene_std::raster::RelativeAbsolute), frame_memo_node!(graphene_std::raster::RelativeAbsolute), lend_node!(graphene_std::raster::SelectiveColorChoice), - clone_out_node!(graphene_std::raster::SelectiveColorChoice), + clone_node!(graphene_std::raster::SelectiveColorChoice), frame_memo_node!(graphene_std::raster::SelectiveColorChoice), lend_node!(graphene_std::vector::misc::GridType), - clone_out_node!(graphene_std::vector::misc::GridType), + clone_node!(graphene_std::vector::misc::GridType), frame_memo_node!(graphene_std::vector::misc::GridType), lend_node!(graphene_std::vector::misc::ArcType), - clone_out_node!(graphene_std::vector::misc::ArcType), + clone_node!(graphene_std::vector::misc::ArcType), frame_memo_node!(graphene_std::vector::misc::ArcType), lend_node!(graphene_std::vector::misc::RowsOrColumns), - clone_out_node!(graphene_std::vector::misc::RowsOrColumns), + clone_node!(graphene_std::vector::misc::RowsOrColumns), frame_memo_node!(graphene_std::vector::misc::RowsOrColumns), lend_node!(graphene_std::vector::misc::MergeByDistanceAlgorithm), - clone_out_node!(graphene_std::vector::misc::MergeByDistanceAlgorithm), + clone_node!(graphene_std::vector::misc::MergeByDistanceAlgorithm), frame_memo_node!(graphene_std::vector::misc::MergeByDistanceAlgorithm), lend_node!(graphene_std::vector::misc::ExtrudeJoiningAlgorithm), - clone_out_node!(graphene_std::vector::misc::ExtrudeJoiningAlgorithm), + clone_node!(graphene_std::vector::misc::ExtrudeJoiningAlgorithm), frame_memo_node!(graphene_std::vector::misc::ExtrudeJoiningAlgorithm), lend_node!(graphene_std::vector::misc::PointSpacingType), - clone_out_node!(graphene_std::vector::misc::PointSpacingType), + clone_node!(graphene_std::vector::misc::PointSpacingType), frame_memo_node!(graphene_std::vector::misc::PointSpacingType), lend_node!(graphene_std::vector::style::StrokeCap), - clone_out_node!(graphene_std::vector::style::StrokeCap), + clone_node!(graphene_std::vector::style::StrokeCap), frame_memo_node!(graphene_std::vector::style::StrokeCap), lend_node!(graphene_std::vector::style::StrokeJoin), - clone_out_node!(graphene_std::vector::style::StrokeJoin), + clone_node!(graphene_std::vector::style::StrokeJoin), frame_memo_node!(graphene_std::vector::style::StrokeJoin), lend_node!(graphene_std::vector::style::StrokeAlign), - clone_out_node!(graphene_std::vector::style::StrokeAlign), + clone_node!(graphene_std::vector::style::StrokeAlign), frame_memo_node!(graphene_std::vector::style::StrokeAlign), lend_node!(graphene_std::vector::style::PaintOrder), - clone_out_node!(graphene_std::vector::style::PaintOrder), + clone_node!(graphene_std::vector::style::PaintOrder), frame_memo_node!(graphene_std::vector::style::PaintOrder), lend_node!(graphene_std::vector::style::GradientType), - clone_out_node!(graphene_std::vector::style::GradientType), + clone_node!(graphene_std::vector::style::GradientType), frame_memo_node!(graphene_std::vector::style::GradientType), lend_node!(graphene_std::vector::style::GradientSpreadMethod), - clone_out_node!(graphene_std::vector::style::GradientSpreadMethod), + clone_node!(graphene_std::vector::style::GradientSpreadMethod), frame_memo_node!(graphene_std::vector::style::GradientSpreadMethod), lend_node!(Option), - clone_out_node!(Option), + clone_node!(Option), frame_memo_node!(Option), lend_node!(graphene_std::transform::ReferencePoint), - clone_out_node!(graphene_std::transform::ReferencePoint), + clone_node!(graphene_std::transform::ReferencePoint), frame_memo_node!(graphene_std::transform::ReferencePoint), lend_node!(graphene_std::vector::misc::CentroidType), - clone_out_node!(graphene_std::vector::misc::CentroidType), + clone_node!(graphene_std::vector::misc::CentroidType), frame_memo_node!(graphene_std::vector::misc::CentroidType), lend_node!(graphene_std::vector::misc::BooleanOperation), - clone_out_node!(graphene_std::vector::misc::BooleanOperation), + clone_node!(graphene_std::vector::misc::BooleanOperation), frame_memo_node!(graphene_std::vector::misc::BooleanOperation), lend_node!(graphene_std::text::TextAlign), - clone_out_node!(graphene_std::text::TextAlign), + clone_node!(graphene_std::text::TextAlign), frame_memo_node!(graphene_std::text::TextAlign), lend_node!(graphene_std::transform::ScaleType), - clone_out_node!(graphene_std::transform::ScaleType), + clone_node!(graphene_std::transform::ScaleType), frame_memo_node!(graphene_std::transform::ScaleType), lend_node!(graphene_std::vector::misc::InterpolationDistribution), - clone_out_node!(graphene_std::vector::misc::InterpolationDistribution), + clone_node!(graphene_std::vector::misc::InterpolationDistribution), frame_memo_node!(graphene_std::vector::misc::InterpolationDistribution), lend_node!(RenderIntermediate), - clone_out_node!(RenderIntermediate), + clone_node!(RenderIntermediate), frame_memo_node!(RenderIntermediate), lend_node!(wgpu_executor::WgpuExecutorHandle), - clone_out_node!(wgpu_executor::WgpuExecutorHandle), + clone_node!(wgpu_executor::WgpuExecutorHandle), frame_memo_node!(wgpu_executor::WgpuExecutorHandle), lend_node!(Option), - clone_out_node!(Option), + clone_node!(Option), frame_memo_node!(Option), lend_node!(wgpu_executor::WgpuPipelineCache), - clone_out_node!(wgpu_executor::WgpuPipelineCache), + clone_node!(wgpu_executor::WgpuPipelineCache), frame_memo_node!(wgpu_executor::WgpuPipelineCache), ]; // ============= @@ -761,7 +761,7 @@ mod node_registry_macros { return Err(ConstructionError::Arity { expected: 1, got: inputs.len() }); } let mut inputs = inputs.into_iter(); - let node = graphene_core::memo::LendNode::<$type, _>::new(inputs.next().unwrap().downcast::<$type>()?); + let node = graphene_core::memo::LendNode::new(inputs.next().unwrap().downcast::<$type>()?); Ok(EdgeHandle::new_ref(std::sync::Arc::new(node) as std::sync::Arc>)) }, }, @@ -769,10 +769,10 @@ mod node_registry_macros { }; } - macro_rules! clone_out_node { + macro_rules! clone_node { ($type:ty) => { ( - ProtoNodeIdentifier::new("graphene_core::memo::CloneOutNode"), + ProtoNodeIdentifier::new("graphene_core::debug::CloneNode"), RegistryEntry { io: NodeIOTypes::new(concrete!(Context), concrete!($type), vec![lend_edge_type::<$type>()]), constructor: |inputs| { @@ -780,7 +780,7 @@ mod node_registry_macros { return Err(ConstructionError::Arity { expected: 1, got: inputs.len() }); } let mut inputs = inputs.into_iter(); - let node = graphene_core::memo::CloneOutNode::<$type, _>::new(inputs.next().unwrap().downcast_lend::<$type>()?); + let node = graphene_core::debug::CloneNode::new(inputs.next().unwrap().downcast_lend::<$type>()?); Ok(EdgeHandle::new(std::sync::Arc::new(node) as std::sync::Arc>)) }, }, @@ -808,7 +808,7 @@ mod node_registry_macros { } pub(crate) use async_node; - pub(crate) use clone_out_node; + pub(crate) use clone_node; pub(crate) use convert_node; pub(crate) use frame_memo_node; pub(crate) use into_node; diff --git a/node-graph/node-macro/src/codegen.rs b/node-graph/node-macro/src/codegen.rs index 90ecc36217..31ca1ef13d 100644 --- a/node-graph/node-macro/src/codegen.rs +++ b/node-graph/node-macro/src/codegen.rs @@ -633,6 +633,27 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn ]); } + let has_lend = parsed.fields.iter().any(|field| matches!(&field.ty, ParsedFieldType::Regular(RegularParsedField { lend: Some(_), .. }))); + let declared_arena_lifetime = ctx_param.and_then(|ctx_param| { + ctx_param.bounds.iter().find_map(|bound| { + let TypeParamBound::Trait(trait_bound) = bound else { return None }; + let segment = trait_bound.path.segments.last()?; + if segment.ident != "ExtractArena" { + return None; + } + let PathArguments::AngleBracketed(args) = &segment.arguments else { return None }; + match args.args.first() { + Some(GenericArgument::Lifetime(lifetime)) => Some(lifetime.clone()), + _ => None, + } + }) + }); + let introduced_lend_lifetime = (has_lend && declared_arena_lifetime.is_none()).then(|| Lifetime::new("'__lend", proc_macro2::Span::call_site())); + let lend_lifetime = declared_arena_lifetime.or_else(|| introduced_lend_lifetime.clone()); + if let Some(lifetime) = &introduced_lend_lifetime { + ctx_bounds.push(quote!(#core_types::context::ExtractArena)); + } + 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"), @@ -655,6 +676,9 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn if ctx_param.is_none() { generics.push(ctx_generic); } + if let Some(lifetime) = &introduced_lend_lifetime { + generics.insert(0, quote!(#lifetime)); + } let fn_name = &parsed.fn_name; let mod_name = format_ident!("_{}_mod", parsed.mod_name); @@ -702,6 +726,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let kernel_params = regular_fields.iter().filter(|field| !injected_name(&field.pat_ident.ident)).map(|field| { let pat = &field.pat_ident; match &field.ty { + ParsedFieldType::Regular(RegularParsedField { ty, lend: Some(_), .. }) => quote!(#pat: &#ty), ParsedFieldType::Regular(RegularParsedField { ty, .. }) => quote!(#pat: #ty), ParsedFieldType::Node(NodeParsedField { output_type, .. }) if raw_lazy => { let bound = lazy_bound(output_type); @@ -715,6 +740,10 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn }); let node_bounds = regular_fields.iter().zip(&node_generics).map(|(field, node_generic)| match &field.ty { + ParsedFieldType::Regular(RegularParsedField { ty, lend: Some(_), .. }) => { + let lifetime = lend_lifetime.as_ref().expect("lend fields imply the lend lifetime"); + quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = &#lifetime #ty>) + } ParsedFieldType::Regular(RegularParsedField { ty, .. }) => quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = #ty>), ParsedFieldType::Node(NodeParsedField { output_type, .. }) => { let bound = lazy_bound(output_type); @@ -722,6 +751,23 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn } }); + let mut lend_outlives: Vec = regular_fields + .iter() + .filter_map(|field| match &field.ty { + ParsedFieldType::Regular(RegularParsedField { ty, lend: Some(_), .. }) => { + let lifetime = lend_lifetime.as_ref().expect("lend fields imply the lend lifetime"); + Some(quote!(#ty: #lifetime)) + } + _ => None, + }) + .collect(); + if let Type::Reference(reference) = &trait_output + && let Some(lifetime) = &reference.lifetime + { + let inner = &reference.elem; + lend_outlives.push(quote!(#inner: #lifetime)); + } + let mut async_bounds = match (async_fn, future_kernel) { (false, false) => Vec::new(), (false, true) => vec![quote!(#trait_output: Clone)], @@ -1007,6 +1053,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn impl<#(#generics,)* #(#node_generics,)*> #core_types::node::Node<#ctx_ident> for #mod_name::#struct_name<#(#struct_type_params,)*> where #(#node_bounds,)* + #(#lend_outlives,)* #(#clampable_bounds,)* #(#async_bounds,)* #(#where_predicates,)* @@ -1181,22 +1228,60 @@ fn entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, data_field_generic return quote!(); } + let ref_output_inner = match slot_value_type(&parsed.output_type) { + Type::Reference(reference) => Some((*reference.elem).clone()), + _ => None, + }; + if let Some(inner) = &ref_output_inner { + let ctx_ident = context_param(parsed).map(|ctx| ctx.ident.clone()); + let open_generics = 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, + }); + if open_generics.into_iter().any(|generic| type_contains_ident(inner, generic)) { + 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 lend_flags: Vec = regular_fields + .iter() + .map(|field| matches!(&field.ty, ParsedFieldType::Regular(RegularParsedField { lend: Some(_), .. }))) + .collect(); let entries = rows.iter().map(|row| { - let types = row.iter(); - let edge_types = row.iter().map(|ty| quote!(gcore::registry::SharedEdge>)); + let input_types = row.iter().zip(&lend_flags).map(|(ty, lend)| match lend { + true => quote!(gcore::registry::lend_edge_type::<#ty>()), + false => quote!(gcore::registry::edge_type::<#ty>()), + }); + let edge_types = row.iter().zip(&lend_flags).map(|(ty, lend)| match lend { + true => quote!(gcore::registry::SharedEdge>), + false => quote!(gcore::registry::SharedEdge>), + }); let output = quote!(<#struct_name<#(#edge_types),*> as gcore::node::Node>>::Output); - let downcasts = names.iter().zip(row.iter()).map(|(name, ty)| quote!(let #name = inputs.next().unwrap().downcast::<#ty>()?;)); + let (io_output, construct) = match &ref_output_inner { + Some(inner) => ( + quote!(gcore::registry::ref_type::<#inner>()), + quote!(Ok(gcore::registry::EdgeHandle::new_ref(::std::sync::Arc::new(#struct_name::new(#(#names),*)) as ::std::sync::Arc>))), + ), + None => ( + quote!(gcore::concrete!(#output)), + quote!(Ok(gcore::registry::EdgeHandle::new(::std::sync::Arc::new(#struct_name::new(#(#names),*)) as ::std::sync::Arc>))), + ), + }; + let downcasts = names.iter().zip(row.iter()).zip(&lend_flags).map(|((name, ty), lend)| match lend { + true => quote!(let #name = inputs.next().unwrap().downcast_lend::<#ty>()?;), + false => quote!(let #name = inputs.next().unwrap().downcast::<#ty>()?;), + }); quote! { gcore::registry::RegistryEntry { io: gcore::registry::NodeIOTypes::new( gcore::concrete!(gcore::context::ContextImpl<'static>), - gcore::concrete!(#output), - vec![#(gcore::registry::edge_type::<#types>()),*], + #io_output, + vec![#(#input_types),*], ), constructor: |inputs| { if inputs.len() != #arity { @@ -1204,7 +1289,7 @@ fn entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, data_field_generic } let mut inputs = inputs.into_iter(); #(#downcasts)* - Ok(gcore::registry::EdgeHandle::new(::std::sync::Arc::new(#struct_name::new(#(#names),*)) as ::std::sync::Arc>)) + #construct }, } } diff --git a/node-graph/node-macro/src/parsing.rs b/node-graph/node-macro/src/parsing.rs index 605b2ba91c..cf3dffc3d0 100644 --- a/node-graph/node-macro/src/parsing.rs +++ b/node-graph/node-macro/src/parsing.rs @@ -240,6 +240,8 @@ impl Parse for NumberRange { #[derive(Clone, Debug)] pub struct RegularParsedField { pub ty: Type, + /// The original reference tokens when the parameter was written `&T`; `ty` holds the peeled inner type. + pub lend: Option, pub exposed: bool, pub value_source: ParsedValueSource, pub number_soft_min: Option, @@ -904,6 +906,11 @@ fn parse_field(pat_ident: PatIdent, ty: Type, attrs: &[Attribute]) -> syn::Resul .transpose()? .unwrap_or_default(); + let (ty, lend) = match ty { + Type::Reference(reference) => ((*reference.elem).clone(), Some(reference)), + ty => (ty, None), + }; + // Error if a float literal is given for a bound on an integer-typed field if is_integer_type(&ty) { let bound_attrs = [ @@ -936,6 +943,7 @@ fn parse_field(pat_ident: PatIdent, ty: Type, attrs: &[Attribute]) -> syn::Resul number_hard_max, number_mode_range, ty, + lend, value_source, implementations, gpu_image, @@ -1048,6 +1056,7 @@ impl ParsedNodeFn { widget_override: ParsedWidgetOverride::Hidden, ty: ParsedFieldType::Regular(RegularParsedField { ty, + lend: None, exposed: false, value_source, number_soft_min: None, @@ -1218,6 +1227,7 @@ mod tests { description: String::new(), widget_override: ParsedWidgetOverride::None, ty: ParsedFieldType::Regular(RegularParsedField { + lend: None, ty: parse_quote!(f64), exposed: false, value_source: ParsedValueSource::None, @@ -1308,6 +1318,7 @@ mod tests { description: String::new(), widget_override: ParsedWidgetOverride::None, ty: ParsedFieldType::Regular(RegularParsedField { + lend: None, ty: parse_quote!(DVec2), exposed: false, value_source: ParsedValueSource::None, @@ -1380,6 +1391,7 @@ mod tests { description: String::new(), widget_override: ParsedWidgetOverride::None, ty: ParsedFieldType::Regular(RegularParsedField { + lend: None, ty: parse_quote!(f64), exposed: false, value_source: ParsedValueSource::Default(quote!(50.)), @@ -1450,6 +1462,7 @@ mod tests { description: String::new(), widget_override: ParsedWidgetOverride::None, ty: ParsedFieldType::Regular(RegularParsedField { + lend: None, ty: parse_quote!(f64), exposed: false, value_source: ParsedValueSource::None, @@ -1532,6 +1545,7 @@ mod tests { description: String::from("b"), widget_override: ParsedWidgetOverride::None, ty: ParsedFieldType::Regular(RegularParsedField { + lend: None, ty: parse_quote!(f64), exposed: false, value_source: ParsedValueSource::None, @@ -1617,6 +1631,7 @@ mod tests { description: String::new(), widget_override: ParsedWidgetOverride::None, ty: ParsedFieldType::Regular(RegularParsedField { + lend: None, ty: parse_quote!(String), exposed: true, value_source: ParsedValueSource::None, 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 1ec06a28e8..f004d162e6 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 @@ -232,6 +232,7 @@ impl PerPixelAdjustCodegen<'_> { widget_override: Default::default(), ty: ParsedFieldType::Regular(RegularParsedField { ty: parse_quote!(#wgpu_executor::WgpuExecutorHandle), + lend: None, exposed: true, value_source: ParsedValueSource::Scope(Box::new(parse_quote!("graphene_std::platform_application_io::WgpuExecutorNode"))), number_soft_min: None, diff --git a/node-graph/node-macro/src/validation.rs b/node-graph/node-macro/src/validation.rs index bc393560f9..d9ea23c382 100644 --- a/node-graph/node-macro/src/validation.rs +++ b/node-graph/node-macro/src/validation.rs @@ -12,6 +12,7 @@ pub fn validate_node_fn(parsed: &ParsedNodeFn) -> syn::Result<()> { validate_min_max, validate_range_slider_bounds, validate_async_source, + validate_lend_fields, ]; for validator in validators { @@ -63,6 +64,30 @@ fn validate_async_source(parsed: &ParsedNodeFn) { } } +fn validate_lend_fields(parsed: &ParsedNodeFn) { + let future_kernel = crate::codegen::is_source_kernel(&parsed.output_type); + for field in &parsed.fields { + let ParsedFieldType::Regular(RegularParsedField { lend: Some(reference), .. }) = &field.ty else { + continue; + }; + if let Some(mutability) = &reference.mutability { + emit_error!(mutability.span(), "reference parameters are read-only lends; `&mut` is not supported"); + } + if let Some(lifetime) = &reference.lifetime { + emit_error!(lifetime.span(), "reference parameters use the eval lifetime implicitly; write a bare `&T`"); + } + if field.is_data_field { + emit_error!(field.pat_ident.span(), "`#[data]` fields are node-resident state and cannot be references"); + } + if parsed.is_async || future_kernel { + emit_error!( + field.pat_ident.span(), + "source kernels move their inputs into the spawned task, so they cannot take reference parameters" + ); + } + } +} + fn validate_min_max(parsed: &ParsedNodeFn) { for field in &parsed.fields { if let ParsedField { diff --git a/node-graph/nodes/gcore/src/debug.rs b/node-graph/nodes/gcore/src/debug.rs index 30b042ddee..fbbdf6639a 100644 --- a/node-graph/nodes/gcore/src/debug.rs +++ b/node-graph/nodes/gcore/src/debug.rs @@ -29,8 +29,8 @@ fn unwrap_option(_: impl Ctx, #[implementations(Option, Option< input.unwrap_or_default() } -/// Meant for debugging purposes, not general use. Clones the input value. +/// Clones the value borrowed from a lending edge. Doubles as the checker-inserted clone-out adapter. #[node_macro::node(category("Debug"))] -fn clone<'i, T: Clone + 'i>(_: impl Ctx, #[implementations(&List>)] value: &'i T) -> T { +fn clone(_: impl Ctx, #[implementations(List>)] value: &T) -> T { value.clone() } diff --git a/node-graph/nodes/gcore/src/memo.rs b/node-graph/nodes/gcore/src/memo.rs index 0570bf8e2d..3b382aaf40 100644 --- a/node-graph/nodes/gcore/src/memo.rs +++ b/node-graph/nodes/gcore/src/memo.rs @@ -103,75 +103,9 @@ pub fn park(arena: &Arena, result: GPoll) -> GPoll<&T> { } /// Adapts an owned edge to a lending one by parking each result in the eval arena. -pub struct LendNode { - content: NodeContent, - _value: std::marker::PhantomData T>, -} - -impl LendNode { - pub fn new(content: NodeContent) -> Self { - Self { - content, - _value: std::marker::PhantomData, - } - } -} - -impl<'e, Input, T, NodeContent> Node for LendNode -where - Input: Ctx + ExtractArena, - T: Send + Sync + 'e, - NodeContent: Node, -{ - type Output = &'e T; - - fn eval(&self, input: &Input) -> GPoll<&'e T> { - park(input.arena(), self.content.eval(input)) - } - - fn extent(&self, input: &Input) -> GPoll { - self.content.extent(input) - } - - fn serialize(&self) -> Option> { - self.content.serialize() - } -} - -/// Adapts a lending edge to an owned one by cloning the borrowed value out. -pub struct CloneOutNode { - content: NodeContent, - _value: std::marker::PhantomData T>, -} - -impl CloneOutNode { - pub fn new(content: NodeContent) -> Self { - Self { - content, - _value: std::marker::PhantomData, - } - } -} - -impl<'e, Input, T, NodeContent> Node for CloneOutNode -where - Input: Ctx + ExtractArena, - T: Clone + 'e, - NodeContent: Node, -{ - type Output = T; - - fn eval(&self, input: &Input) -> GPoll { - self.content.eval(input).map(Clone::clone) - } - - fn extent(&self, input: &Input) -> GPoll { - self.content.extent(input) - } - - fn serialize(&self) -> Option> { - self.content.serialize() - } +#[node_macro::node(category(""), path(graphene_core::memo), skip_impl)] +fn lend<'e, T: Send + Sync>(ctx: impl Ctx + ExtractArena<'e>, value: T) -> GPoll<&'e T> { + park(ctx.arena(), GPoll::Final(value)) } type MonitorValue = Arc>>>>; diff --git a/node-graph/nodes/raster/src/image_color_palette.rs b/node-graph/nodes/raster/src/image_color_palette.rs index b91e487f18..377810c6c0 100644 --- a/node-graph/nodes/raster/src/image_color_palette.rs +++ b/node-graph/nodes/raster/src/image_color_palette.rs @@ -6,7 +6,7 @@ use raster_types::{CPU, Raster}; #[node_macro::node(category("Color"))] fn image_color_palette( _: impl Ctx, - image: List>, + image: &List>, #[default(4)] #[hard(1..)] count: u32,