From 9b45af854a587bfabc29cf77b35bca0af2025f66 Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Mon, 3 Aug 2026 16:33:27 +0000 Subject: [PATCH] Emit Ref wire types for lending rows and splice lend/clone_out adapters in the type checker --- node-graph/graph-craft/src/proto.rs | 297 ++++++++++++++-- .../src/dynamic_executor.rs | 57 ++- .../interpreted-executor/src/node_registry.rs | 328 +++++++++++++++++- .../libraries/core-types/src/registry.rs | 6 +- node-graph/nodes/gcore/src/memo.rs | 74 +++- 5 files changed, 723 insertions(+), 39 deletions(-) diff --git a/node-graph/graph-craft/src/proto.rs b/node-graph/graph-craft/src/proto.rs index 52132b5d3f..d4c999f4cf 100644 --- a/node-graph/graph-craft/src/proto.rs +++ b/node-graph/graph-craft/src/proto.rs @@ -369,6 +369,40 @@ impl ProtoNetwork { nullification_node_id } + /// Splices the selected `Ref` adapters between the consumer at `consumer_index` and its + /// producers, keeping the node vec topologically sorted and ids stable. `materialized` carries + /// the adapter ids already spliced during this update so shared producers get one adapter. + fn insert_ref_adapters(&mut self, consumer_index: usize, adapters: &[RefAdapter], materialized: &mut HashSet) { + let mut consumer_index = consumer_index; + for adapter in adapters { + let (_, consumer) = &self.nodes[consumer_index]; + let producer = consumer.unwrap_construction_nodes()[adapter.input_index]; + let mut path = consumer.original_location.path.clone(); + + // A path extension with a placeholder value which should not conflict with existing paths + if let Some(path) = path.as_mut() { + path.push(NodeId(11)); + } + + let node = ProtoNode { + construction_args: ConstructionArgs::Nodes(vec![producer]), + call_argument: concrete!(Context), + identifier: adapter.identifier.clone(), + original_location: OriginalLocation { path, ..Default::default() }, + ..Default::default() + }; + let id = node.stable_node_id().expect("adapter nodes always produce a stable id"); + if materialized.insert(id) { + self.nodes.insert(consumer_index, (id, node)); + consumer_index += 1; + } + let (_, consumer) = &mut self.nodes[consumer_index]; + if let ConstructionArgs::Nodes(args) = &mut consumer.construction_args { + args[adapter.input_index] = id; + } + } + } + fn find_context_dependencies(&mut self, id: NodeId) -> (ContextModification, Option) { let mut branch_dependencies = Vec::new(); let mut combined_deps = ContextModification::default(); @@ -657,15 +691,39 @@ impl TypingContext { /// Updates the `TypingContext` with a given proto network. This will infer the types of the nodes /// and store them in the `inferred` field. The proto network has to be topologically sorted - /// and contain fully resolved stable node ids. - pub fn update(&mut self, network: &ProtoNetwork) -> Result<(), GraphErrors> { - for (id, node) in network.nodes.iter() { - self.infer(*id, node)?; + /// and contain fully resolved stable node ids. When a node's inputs disagree with every + /// implementation only on `Ref`-ness, the matching lend/clone_out adapter is spliced into the + /// network and inference resumes with the adapter in place. + pub fn update(&mut self, network: &mut ProtoNetwork) -> Result<(), GraphErrors> { + let mut materialized_adapters = HashSet::new(); + let mut index = 0; + while index < network.nodes.len() { + let (id, node) = &network.nodes[index]; + match self.infer(*id, node) { + Ok(_) => index += 1, + Err(errors) => match self.select_ref_adapters(node) { + Some(adapters) => network.insert_ref_adapters(index, &adapters, &mut materialized_adapters), + None => return Err(errors), + }, + } } Ok(()) } + fn select_ref_adapters(&self, node: &ProtoNode) -> Option> { + let ConstructionArgs::Nodes(args) = &node.construction_args else { return None }; + let inputs = args.iter().map(|id| self.inferred.get(id).map(NodeIOTypes::ty)).collect::>>()?; + let candidates: Vec<&NodeIOTypes> = self.lookup.get(&node.identifier)?.iter().map(|entry| &entry.io).collect(); + let adapters = select_ref_adapters_over(&node.call_argument, &inputs, &candidates)?; + let constructible = adapters.iter().all(|adapter| { + self.lookup + .get(&adapter.identifier) + .is_some_and(|entries| entries.iter().any(|entry| entry.io.inputs.len() == 1 && valid_type(&inputs[adapter.input_index], &entry.io.inputs[0]))) + }); + constructible.then_some(adapters) + } + pub fn remove_inference(&mut self, node_id: NodeId) -> Option { self.constructor.remove(&node_id); self.inferred.remove(&node_id) @@ -721,33 +779,6 @@ impl TypingContext { return Err(vec![GraphError::new(node, GraphErrorType::UnexpectedGenerics { index, inputs })]); } - /// Checks if a proposed input to a particular (primary or secondary) input connector is valid for its type signature. - /// `from` indicates the value given to a input, `to` indicates the input's allowed type as specified by its type signature. - fn valid_type(from: &Type, to: &Type) -> bool { - match (from, to) { - // Direct comparison of two concrete types. - (Type::Concrete(type1), Type::Concrete(type2)) => type1 == type2, - // Direct comparison of two function types. - // Note: in the presence of subtyping, functions are considered on a "greater than or equal to" basis of its function type's generality. - // That means we compare their types with a contravariant relationship, which means that a more general type signature may be substituted for a more specific type signature. - // For example, we allow `T -> V` to be substituted with `T' -> V` or `() -> V` where T' and () are more specific than T. - // This allows us to supply anything to a function that is satisfied with `()`. - // In other words, we are implementing these two relations, where the >= operator means that the left side is more general than the right side: - // - `T >= T' ⇒ (T' -> V) >= (T -> V)` (functions are contravariant in their input types) - // - `V >= V' ⇒ (T -> V) >= (T -> V')` (functions are covariant in their output types) - // While these two relations aren't a truth about the universe, they are a design decision that we are employing in our language design that is also common in other languages. - // For example, Rust implements these same relations as it describes here: - // Graphite doesn't have subtyping currently, but it used to have it, and may do so again, so we make sure to compare types in this way to make things easier. - // More details explained here: - (Type::Fn(in1, out1), Type::Fn(in2, out2)) => valid_type(out2, out1) && valid_type(in1, in2), - // If either the proposed input or the allowed input are generic, we allow the substitution (meaning this is a valid subtype). - // TODO: Add proper generic counting which is not based on the name - (Type::Generic(_), _) | (_, Type::Generic(_)) => true, - // Reject unknown type relationships. - _ => false, - } - } - // List of all implementations that match the input types let valid_output_types = candidates .iter() @@ -850,6 +881,87 @@ impl TypingContext { } } +/// Checks if a proposed input to a particular (primary or secondary) input connector is valid for its type signature. +/// `from` indicates the value given to a input, `to` indicates the input's allowed type as specified by its type signature. +fn valid_type(from: &Type, to: &Type) -> bool { + match (from, to) { + // Direct comparison of two concrete types. + (Type::Concrete(type1), Type::Concrete(type2)) => type1 == type2, + // Direct comparison of two function types. + // Note: in the presence of subtyping, functions are considered on a "greater than or equal to" basis of its function type's generality. + // That means we compare their types with a contravariant relationship, which means that a more general type signature may be substituted for a more specific type signature. + // For example, we allow `T -> V` to be substituted with `T' -> V` or `() -> V` where T' and () are more specific than T. + // This allows us to supply anything to a function that is satisfied with `()`. + // In other words, we are implementing these two relations, where the >= operator means that the left side is more general than the right side: + // - `T >= T' ⇒ (T' -> V) >= (T -> V)` (functions are contravariant in their input types) + // - `V >= V' ⇒ (T -> V) >= (T -> V')` (functions are covariant in their output types) + // While these two relations aren't a truth about the universe, they are a design decision that we are employing in our language design that is also common in other languages. + // For example, Rust implements these same relations as it describes here: + // Graphite doesn't have subtyping currently, but it used to have it, and may do so again, so we make sure to compare types in this way to make things easier. + // More details explained here: + (Type::Fn(in1, out1), Type::Fn(in2, out2)) => valid_type(out2, out1) && valid_type(in1, in2), + // A lend edge is substitutable exactly when the lent values are. + (Type::Ref(in1), Type::Ref(in2)) => valid_type(in1, in2), + // If either the proposed input or the allowed input are generic, we allow the substitution (meaning this is a valid subtype). + // TODO: Add proper generic counting which is not based on the name + (Type::Generic(_), _) | (_, Type::Generic(_)) => true, + // Reject unknown type relationships. + _ => false, + } +} + +#[derive(Clone, Debug)] +struct RefAdapter { + input_index: usize, + identifier: ProtoNodeIdentifier, +} + +/// Picks the lend or clone_out adapter that reconciles a proposed edge with a wanted edge when they +/// differ only by one `Ref` layer around a concrete output. +fn ref_adapter(proposed: &Type, wanted: &Type) -> Option { + let (Type::Fn(_, proposed_output), Type::Fn(_, wanted_output)) = (proposed, wanted) else { + 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")), + (proposed_output @ Type::Concrete(_), Type::Ref(inner)) if valid_type(proposed_output, inner) => Some(ProtoNodeIdentifier::new("graphene_core::memo::LendNode")), + _ => None, + } +} + +/// Selects adapters for the single implementation the inputs match modulo `Ref`-ness, or `None` +/// when no or several implementations do. +fn select_ref_adapters_over(call_argument: &Type, inputs: &[Type], candidates: &[&NodeIOTypes]) -> Option> { + let mut selected = None; + for candidate in candidates { + if !valid_type(&candidate.call_argument, call_argument) || candidate.inputs.len() != inputs.len() { + continue; + } + let mut adapters = Vec::new(); + let mut fits = true; + for (input_index, (proposed, wanted)) in inputs.iter().zip(&candidate.inputs).enumerate() { + if valid_type(proposed, wanted) { + continue; + } + match ref_adapter(proposed, wanted) { + Some(identifier) => adapters.push(RefAdapter { input_index, identifier }), + None => { + fits = false; + break; + } + } + } + if !fits || adapters.is_empty() { + continue; + } + if selected.is_some() { + return None; + } + selected = Some(adapters); + } + selected +} + /// Returns a list of all generic types used in the node fn collect_generics(types: &NodeIOTypes) -> Vec> { let inputs = [&types.call_argument].into_iter().chain(types.inputs.iter().map(|x| x.nested_type())); @@ -1161,3 +1273,126 @@ mod test { } } } + +#[cfg(test)] +mod ref_adapter_test { + use super::*; + + fn adapter_lookup() -> &'static HashMap> { + static LOOKUP: std::sync::LazyLock>> = std::sync::LazyLock::new(|| { + let unused: NodeConstructor = |_| Err(ConstructionError::Arity { expected: 0, got: 0 }); + [ + ( + ProtoNodeIdentifier::new("wants_ref"), + vec![RegistryEntry { + io: NodeIOTypes::new(concrete!(Context), concrete!(String), vec![lend_edge_type::()]), + constructor: unused, + }], + ), + ( + ProtoNodeIdentifier::new("wants_ref_ambiguously"), + vec![ + RegistryEntry { + io: NodeIOTypes::new(concrete!(Context), concrete!(String), vec![lend_edge_type::()]), + constructor: unused, + }, + RegistryEntry { + io: NodeIOTypes::new(concrete!(Context), concrete!(f64), vec![lend_edge_type::()]), + constructor: unused, + }, + ], + ), + ( + ProtoNodeIdentifier::new("graphene_core::memo::LendNode"), + vec![RegistryEntry { + io: NodeIOTypes::new(concrete!(Context), ref_type::(), vec![edge_type::()]), + constructor: unused, + }], + ), + ] + .into_iter() + .collect() + }); + &LOOKUP + } + + fn string_value() -> ProtoNode { + ProtoNode::value(ConstructionArgs::Value(TaggedValue::String("lent".to_string()).into()), vec![]) + } + + fn consumer(identifier: &str, producer: NodeId) -> ProtoNode { + ProtoNode { + identifier: ProtoNodeIdentifier::with_owned_string(identifier.to_string()), + call_argument: concrete!(Context), + construction_args: ConstructionArgs::Nodes(vec![producer]), + ..Default::default() + } + } + + #[test] + fn a_ref_only_mismatch_splices_the_lend_adapter() { + let mut network = ProtoNetwork { + inputs: vec![], + output: NodeId(1), + nodes: vec![(NodeId(0), string_value()), (NodeId(1), consumer("wants_ref", NodeId(0)))], + }; + + let mut typing = TypingContext::new(adapter_lookup()); + typing.update(&mut network).unwrap(); + + assert_eq!(network.nodes.len(), 3); + let (adapter_id, adapter) = &network.nodes[1]; + assert_eq!(adapter.identifier.as_str(), "graphene_core::memo::LendNode"); + assert_eq!(adapter.unwrap_construction_nodes(), vec![NodeId(0)]); + assert_eq!(network.nodes[2].1.unwrap_construction_nodes(), vec![*adapter_id]); + assert_eq!(typing.type_of(*adapter_id).unwrap().return_value, ref_type::()); + assert_eq!(typing.type_of(NodeId(1)).unwrap().return_value, concrete!(String)); + } + + #[test] + fn consumers_sharing_a_producer_share_one_adapter() { + let mut network = ProtoNetwork { + inputs: vec![], + output: NodeId(2), + nodes: vec![ + (NodeId(0), string_value()), + (NodeId(1), consumer("wants_ref", NodeId(0))), + (NodeId(2), consumer("wants_ref", NodeId(0))), + ], + }; + + let mut typing = TypingContext::new(adapter_lookup()); + typing.update(&mut network).unwrap(); + + assert_eq!(network.nodes.len(), 4); + let adapters: Vec = network + .nodes + .iter() + .filter(|(_, node)| node.identifier.as_str() == "graphene_core::memo::LendNode") + .map(|(id, _)| *id) + .collect(); + let [adapter_id] = adapters.as_slice() else { + panic!("expected exactly one shared adapter, got {adapters:?}"); + }; + let consumers: Vec> = network + .nodes + .iter() + .filter(|(_, node)| node.identifier.as_str() == "wants_ref") + .map(|(_, node)| node.unwrap_construction_nodes()) + .collect(); + assert_eq!(consumers, vec![vec![*adapter_id], vec![*adapter_id]]); + } + + #[test] + fn an_ambiguous_ref_mismatch_stays_an_error() { + let mut network = ProtoNetwork { + inputs: vec![], + output: NodeId(1), + nodes: vec![(NodeId(0), string_value()), (NodeId(1), consumer("wants_ref_ambiguously", NodeId(0)))], + }; + + let mut typing = TypingContext::new(adapter_lookup()); + assert!(typing.update(&mut network).is_err()); + assert_eq!(network.nodes.len(), 2, "an ambiguous mismatch must not materialize adapters"); + } +} diff --git a/node-graph/interpreted-executor/src/dynamic_executor.rs b/node-graph/interpreted-executor/src/dynamic_executor.rs index 536e7b6a01..e6d1dbae28 100644 --- a/node-graph/interpreted-executor/src/dynamic_executor.rs +++ b/node-graph/interpreted-executor/src/dynamic_executor.rs @@ -71,9 +71,9 @@ pub struct ResolvedDocumentNodeTypesDelta { } impl DynamicExecutor { - pub fn new(proto_network: ProtoNetwork) -> Result { + pub fn new(mut proto_network: ProtoNetwork) -> Result { let mut typing_context = TypingContext::new(&node_registry::NODE_REGISTRY); - typing_context.update(&proto_network)?; + typing_context.update(&mut proto_network)?; let output = proto_network.output; let sources = proto_network.source_ids(); let tree = BorrowTree::new(proto_network, &typing_context)?; @@ -102,9 +102,9 @@ impl DynamicExecutor { /// Updates the existing [`BorrowTree`] to reflect the new [`ProtoNetwork`], reusing nodes where possible. #[cfg_attr(debug_assertions, inline(never))] - pub fn update(&mut self, proto_network: ProtoNetwork) -> Result { + pub fn update(&mut self, mut proto_network: ProtoNetwork) -> Result { self.output = proto_network.output; - self.typing_context.update(&proto_network).map_err(|e| { + self.typing_context.update(&mut proto_network).map_err(|e| { // If there is an error then get types that have been resolved before the error let add = proto_network .nodes @@ -546,4 +546,53 @@ mod test { let result: Option> = tree.eval(NodeId(0), &ctx); assert_eq!(result, Some(GPoll::Final(2))); } + + fn proto_node(identifier: &'static str, args: Vec) -> ProtoNode { + let mut node = ProtoNode::default(); + node.identifier = graph_craft::ProtoNodeIdentifier::new(identifier); + node.call_argument = core_types::concrete!(core_types::context::Context); + node.construction_args = ConstructionArgs::Nodes(args); + node + } + + fn string_value(value: &str) -> ProtoNode { + ProtoNode::value(ConstructionArgs::Value(TaggedValue::String(value.to_string()).into()), vec![]) + } + + #[test] + fn a_lend_adapter_is_spliced_between_a_value_and_a_ref_consumer() { + let network = ProtoNetwork { + inputs: vec![], + output: NodeId(1), + nodes: vec![ + (NodeId(0), string_value("lent")), + (NodeId(1), proto_node("graphene_core::memo::CloneOutNode", vec![NodeId(0)])), + ], + }; + + let executor = DynamicExecutor::new(network).unwrap(); + assert_eq!((&executor).execute(()).unwrap(), GPoll::Final(TaggedValue::String("lent".to_string()))); + } + + #[test] + fn a_clone_out_adapter_is_spliced_between_a_lending_producer_and_an_owned_consumer() { + let network = ProtoNetwork { + inputs: vec![], + output: NodeId(3), + nodes: vec![ + (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)])), + ], + }; + + let executor = DynamicExecutor::new(network).unwrap(); + assert_eq!((&executor).execute(()).unwrap(), GPoll::Final(TaggedValue::String("memoized".to_string()))); + assert_eq!( + (&executor).execute(()).unwrap(), + GPoll::Final(TaggedValue::String("memoized".to_string())), + "the frame memo lend path must replay across evaluations" + ); + } } diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index f48005aafe..35673fa580 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -13,14 +13,14 @@ use graphene_std::raster::GPU; use graphene_std::raster::color::Color; use graphene_std::raster::*; use graphene_std::raster::{CPU, Raster}; -use graphene_std::registry::{ConstructionError, EdgeHandle, ErasedNode, NodeIOTypes, RegistryEntry}; +use graphene_std::registry::{ConstructionError, EdgeHandle, ErasedLendNode, ErasedNode, NodeIOTypes, RegistryEntry, lend_edge_type, ref_type}; use graphene_std::render_node::RenderIntermediate; use graphene_std::runtime::RuntimeHandle; 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, convert_node, into_node}; +use node_registry_macros::{async_node, clone_out_node, convert_node, frame_memo_node, into_node, lend_node}; use std::collections::HashMap; #[cfg(feature = "gpu")] use wgpu_executor::WgpuExecutorHandle; @@ -295,6 +295,270 @@ fn node_registry() -> HashMap> { async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => wgpu_executor::WgpuExecutorHandle]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Option]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => wgpu_executor::WgpuPipelineCache]), + // ============ + // REF ADAPTERS + // ============ + lend_node!(()), + clone_out_node!(()), + frame_memo_node!(()), + lend_node!(RuntimeHandle), + clone_out_node!(RuntimeHandle), + frame_memo_node!(RuntimeHandle), + lend_node!(SourceId), + clone_out_node!(SourceId), + frame_memo_node!(SourceId), + lend_node!(bool), + clone_out_node!(bool), + frame_memo_node!(bool), + lend_node!(List), + clone_out_node!(List), + frame_memo_node!(List), + lend_node!(List), + clone_out_node!(List), + frame_memo_node!(List), + lend_node!(List), + clone_out_node!(List), + frame_memo_node!(List), + lend_node!(List>), + clone_out_node!(List>), + frame_memo_node!(List>), + lend_node!(List), + clone_out_node!(List), + frame_memo_node!(List), + lend_node!(Image), + clone_out_node!(Image), + frame_memo_node!(Image), + lend_node!(List), + clone_out_node!(List), + frame_memo_node!(List), + lend_node!(List), + clone_out_node!(List), + frame_memo_node!(List), + lend_node!(List), + clone_out_node!(List), + frame_memo_node!(List), + lend_node!(List), + clone_out_node!(List), + frame_memo_node!(List), + lend_node!(List), + clone_out_node!(List), + frame_memo_node!(List), + lend_node!(List), + clone_out_node!(List), + frame_memo_node!(List), + lend_node!(List), + clone_out_node!(List), + frame_memo_node!(List), + lend_node!(List), + clone_out_node!(List), + frame_memo_node!(List), + lend_node!(List), + clone_out_node!(List), + frame_memo_node!(List), + lend_node!(List), + clone_out_node!(List), + frame_memo_node!(List), + lend_node!(AttributeDyn), + clone_out_node!(AttributeDyn), + frame_memo_node!(AttributeDyn), + lend_node!(AttributeValueDyn), + clone_out_node!(AttributeValueDyn), + frame_memo_node!(AttributeValueDyn), + lend_node!(ListDyn), + clone_out_node!(ListDyn), + frame_memo_node!(ListDyn), + #[cfg(target_family = "wasm")] + lend_node!(CanvasHandle), + #[cfg(target_family = "wasm")] + clone_out_node!(CanvasHandle), + #[cfg(target_family = "wasm")] + frame_memo_node!(CanvasHandle), + lend_node!(f64), + clone_out_node!(f64), + frame_memo_node!(f64), + lend_node!(f32), + clone_out_node!(f32), + frame_memo_node!(f32), + lend_node!(u32), + clone_out_node!(u32), + frame_memo_node!(u32), + lend_node!(u64), + clone_out_node!(u64), + frame_memo_node!(u64), + lend_node!(DVec2), + clone_out_node!(DVec2), + frame_memo_node!(DVec2), + lend_node!(String), + clone_out_node!(String), + frame_memo_node!(String), + lend_node!(DAffine2), + clone_out_node!(DAffine2), + frame_memo_node!(DAffine2), + lend_node!(Footprint), + clone_out_node!(Footprint), + frame_memo_node!(Footprint), + lend_node!(RenderOutput), + clone_out_node!(RenderOutput), + frame_memo_node!(RenderOutput), + lend_node!(std::sync::Arc), + clone_out_node!(std::sync::Arc), + frame_memo_node!(std::sync::Arc), + #[cfg(feature = "gpu")] + lend_node!(List>), + #[cfg(feature = "gpu")] + clone_out_node!(List>), + #[cfg(feature = "gpu")] + frame_memo_node!(List>), + lend_node!(Option), + clone_out_node!(Option), + frame_memo_node!(Option), + lend_node!(Option), + clone_out_node!(Option), + frame_memo_node!(Option), + lend_node!(Graphic), + clone_out_node!(Graphic), + frame_memo_node!(Graphic), + lend_node!(glam::f32::Vec2), + clone_out_node!(glam::f32::Vec2), + frame_memo_node!(glam::f32::Vec2), + lend_node!(glam::f32::Affine2), + clone_out_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), + frame_memo_node!(graphene_std::vector::style::Stroke), + lend_node!(graphene_std::text::Font), + clone_out_node!(graphene_std::text::Font), + frame_memo_node!(graphene_std::text::Font), + lend_node!(List), + clone_out_node!(List), + frame_memo_node!(List), + lend_node!(DocumentNode), + clone_out_node!(DocumentNode), + frame_memo_node!(DocumentNode), + lend_node!(graphene_std::ContextModification), + clone_out_node!(graphene_std::ContextModification), + frame_memo_node!(graphene_std::ContextModification), + lend_node!(graphene_std::transform::Footprint), + clone_out_node!(graphene_std::transform::Footprint), + frame_memo_node!(graphene_std::transform::Footprint), + lend_node!(Box), + clone_out_node!(Box), + frame_memo_node!(Box), + lend_node!(graphene_std::blending::BlendMode), + clone_out_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), + frame_memo_node!(graphene_std::raster::LuminanceCalculation), + lend_node!(graphene_std::vector::QRCodeErrorCorrectionLevel), + clone_out_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), + frame_memo_node!(graphene_std::extract_xy::XY), + lend_node!(graphene_std::text_nodes::StringCapitalization), + clone_out_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), + frame_memo_node!(graphene_std::raster::RedGreenBlue), + lend_node!(graphene_std::raster::RedGreenBlueAlpha), + clone_out_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), + frame_memo_node!(graphene_std::animation::RealTimeMode), + lend_node!(graphene_std::raster::NoiseType), + clone_out_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), + frame_memo_node!(graphene_std::raster::FractalType), + lend_node!(graphene_std::raster::CellularDistanceFunction), + clone_out_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), + frame_memo_node!(graphene_std::raster::CellularReturnType), + lend_node!(graphene_std::raster::DomainWarpType), + clone_out_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), + frame_memo_node!(graphene_std::raster::RelativeAbsolute), + lend_node!(graphene_std::raster::SelectiveColorChoice), + clone_out_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), + frame_memo_node!(graphene_std::vector::misc::GridType), + lend_node!(graphene_std::vector::misc::ArcType), + clone_out_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), + frame_memo_node!(graphene_std::vector::misc::RowsOrColumns), + lend_node!(graphene_std::vector::misc::MergeByDistanceAlgorithm), + clone_out_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), + frame_memo_node!(graphene_std::vector::misc::ExtrudeJoiningAlgorithm), + lend_node!(graphene_std::vector::misc::PointSpacingType), + clone_out_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), + frame_memo_node!(graphene_std::vector::style::StrokeCap), + lend_node!(graphene_std::vector::style::StrokeJoin), + clone_out_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), + frame_memo_node!(graphene_std::vector::style::StrokeAlign), + lend_node!(graphene_std::vector::style::PaintOrder), + clone_out_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), + frame_memo_node!(graphene_std::vector::style::GradientType), + lend_node!(graphene_std::vector::style::GradientSpreadMethod), + clone_out_node!(graphene_std::vector::style::GradientSpreadMethod), + frame_memo_node!(graphene_std::vector::style::GradientSpreadMethod), + lend_node!(Option), + clone_out_node!(Option), + frame_memo_node!(Option), + lend_node!(graphene_std::transform::ReferencePoint), + clone_out_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), + frame_memo_node!(graphene_std::vector::misc::CentroidType), + lend_node!(graphene_std::vector::misc::BooleanOperation), + clone_out_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), + frame_memo_node!(graphene_std::text::TextAlign), + lend_node!(graphene_std::transform::ScaleType), + clone_out_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), + frame_memo_node!(graphene_std::vector::misc::InterpolationDistribution), + lend_node!(RenderIntermediate), + clone_out_node!(RenderIntermediate), + frame_memo_node!(RenderIntermediate), + lend_node!(wgpu_executor::WgpuExecutorHandle), + clone_out_node!(wgpu_executor::WgpuExecutorHandle), + frame_memo_node!(wgpu_executor::WgpuExecutorHandle), + lend_node!(Option), + clone_out_node!(Option), + frame_memo_node!(Option), + lend_node!(wgpu_executor::WgpuPipelineCache), + clone_out_node!(wgpu_executor::WgpuPipelineCache), + frame_memo_node!(wgpu_executor::WgpuPipelineCache), ]; // ============= // CONVERT NODES @@ -486,7 +750,67 @@ mod node_registry_macros { }; } + macro_rules! lend_node { + ($type:ty) => { + ( + ProtoNodeIdentifier::new("graphene_core::memo::LendNode"), + RegistryEntry { + io: NodeIOTypes::new(concrete!(Context), ref_type::<$type>(), vec![fn_type!(Context, $type)]), + constructor: |inputs| { + if inputs.len() != 1 { + 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>()?); + Ok(EdgeHandle::new_ref(std::sync::Arc::new(node) as std::sync::Arc>)) + }, + }, + ) + }; + } + + macro_rules! clone_out_node { + ($type:ty) => { + ( + ProtoNodeIdentifier::new("graphene_core::memo::CloneOutNode"), + RegistryEntry { + io: NodeIOTypes::new(concrete!(Context), concrete!($type), vec![lend_edge_type::<$type>()]), + constructor: |inputs| { + if inputs.len() != 1 { + 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>()?); + Ok(EdgeHandle::new(std::sync::Arc::new(node) as std::sync::Arc>)) + }, + }, + ) + }; + } + + macro_rules! frame_memo_node { + ($type:ty) => { + ( + ProtoNodeIdentifier::new("graphene_core::memo::FrameMemoNode"), + RegistryEntry { + io: NodeIOTypes::new(concrete!(Context), ref_type::<$type>(), vec![fn_type!(Context, $type)]), + constructor: |inputs| { + if inputs.len() != 1 { + return Err(ConstructionError::Arity { expected: 1, got: inputs.len() }); + } + let mut inputs = inputs.into_iter(); + let node = graphene_core::memo::FrameMemoNode::new(inputs.next().unwrap().downcast::<$type>()?); + Ok(EdgeHandle::new_ref(std::sync::Arc::new(node) as std::sync::Arc>)) + }, + }, + ) + }; + } + pub(crate) use async_node; + pub(crate) use clone_out_node; pub(crate) use convert_node; + pub(crate) use frame_memo_node; pub(crate) use into_node; + pub(crate) use lend_node; } diff --git a/node-graph/libraries/core-types/src/registry.rs b/node-graph/libraries/core-types/src/registry.rs index 18e32700e7..fb42062fb1 100644 --- a/node-graph/libraries/core-types/src/registry.rs +++ b/node-graph/libraries/core-types/src/registry.rs @@ -88,8 +88,12 @@ pub fn edge_type() -> Type { Type::Fn(Box::new(concrete!(Context)), Box::new(concrete!(T))) } +pub fn ref_type() -> Type { + Type::Ref(Box::new(concrete!(T))) +} + pub fn lend_edge_type() -> Type { - Type::Fn(Box::new(concrete!(Context)), Box::new(Type::Ref(Box::new(concrete!(T))))) + Type::Fn(Box::new(concrete!(Context)), Box::new(ref_type::())) } pub fn cache_key(ctx: &C) -> u64 { diff --git a/node-graph/nodes/gcore/src/memo.rs b/node-graph/nodes/gcore/src/memo.rs index 7066eb9db5..0570bf8e2d 100644 --- a/node-graph/nodes/gcore/src/memo.rs +++ b/node-graph/nodes/gcore/src/memo.rs @@ -1,5 +1,5 @@ use core_types::arena::{Arena, ArenaCell}; -use core_types::context::{Ctx, CtxSnapshot, DeriveCtx, ExtractAll}; +use core_types::context::{Ctx, CtxSnapshot, DeriveCtx, ExtractAll, ExtractArena}; use core_types::frame_table::{FrameTable, Lookup}; use core_types::gpoll::{Extent, Finality, GPoll, Interrupt}; use core_types::graphene_hash::CacheHash; @@ -102,6 +102,78 @@ 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() + } +} + type MonitorValue = Arc>>>>; /// The Monitor node is used by the editor to access the data flowing through it.