diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index 70eb6a95f5..eb2e6fade7 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -167,6 +167,11 @@ impl NodeRuntime { } } + #[cfg(test)] + pub fn take_dirty(&self) -> bool { + self.executor.take_dirty() + } + pub async fn run(&mut self) -> Option { let mut preferences = None; let mut graph = None; diff --git a/editor/src/test_utils.rs b/editor/src/test_utils.rs index 84f67f0418..25d196d228 100644 --- a/editor/src/test_utils.rs +++ b/editor/src/test_utils.rs @@ -53,6 +53,22 @@ impl EditorTestUtils { } runtime.run().await; + // An async source reports `Pending` on the evaluation that starts it and marks the runtime dirty + // once it completes, so the value only reaches the render on a follow-up evaluation. That first + // response is superseded, so it is drained rather than asserted on. + while runtime.take_dirty() { + let _ = editor.poll_node_graph_evaluation(&mut VecDeque::new()); + + let portfolio = &mut editor.dispatcher.message_handlers.portfolio_message_handler; + let (executor, documents) = (&mut portfolio.executor, &mut portfolio.documents); + let document = documents.get_mut(&document_id).unwrap(); + + if let Err(e) = executor.submit_current_node_graph_evaluation(document, document_id, UVec2::ONE, 1., Default::default(), DVec2::ZERO) { + return Err(format!("submit_current_node_graph_evaluation failed\n\n{e}")); + } + runtime.run().await; + } + let mut messages = VecDeque::new(); if let Err(e) = editor.poll_node_graph_evaluation(&mut messages) { return Err(format!("Graph should render\n\n{e}")); diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index 172b822394..f48005aafe 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -189,11 +189,16 @@ fn node_registry() -> HashMap> { // Context nullification #[cfg(feature = "gpu")] async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => std::sync::Arc, Context => graphene_std::ContextModification]), + async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => RuntimeHandle, Context => graphene_std::ContextModification]), + async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => SourceId, Context => graphene_std::ContextModification]), async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => RenderIntermediate, Context => graphene_std::ContextModification]), async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => RenderOutput, Context => graphene_std::ContextModification]), async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => AttributeDyn, Context => graphene_std::ContextModification]), async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => AttributeValueDyn, Context => graphene_std::ContextModification]), async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => ListDyn, Context => graphene_std::ContextModification]), + async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::GradientType, Context => graphene_std::ContextModification]), + async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::GradientSpreadMethod, Context => graphene_std::ContextModification]), + async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Option, Context => graphene_std::ContextModification]), #[cfg(target_family = "wasm")] async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => CanvasHandle, Context => graphene_std::ContextModification]), async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => std::sync::Arc, Context => graphene_std::ContextModification]), @@ -204,6 +209,8 @@ fn node_registry() -> HashMap> { // MEMO NODES // ========== async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => ()]), + async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => RuntimeHandle]), + async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => SourceId]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => bool]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List]), @@ -276,6 +283,8 @@ fn node_registry() -> HashMap> { async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::StrokeAlign]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::PaintOrder]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::GradientType]), + async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::GradientSpreadMethod]), + async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Option]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::transform::ReferencePoint]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::CentroidType]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::BooleanOperation]), diff --git a/node-graph/libraries/core-types/src/runtime.rs b/node-graph/libraries/core-types/src/runtime.rs index 2634321242..45fa051721 100644 --- a/node-graph/libraries/core-types/src/runtime.rs +++ b/node-graph/libraries/core-types/src/runtime.rs @@ -59,12 +59,15 @@ impl Spawner for Box { } } -/// Dropped tasks never complete. +/// Polls each task once inline. Tasks that are not immediately ready never complete. pub struct NoopSpawner; impl Spawner for NoopSpawner { - fn spawn(&self, _task: SourceFuture) { - log::warn!("async source spawned before a host spawner is wired; the task is dropped"); + fn spawn(&self, mut task: SourceFuture) { + let mut context = std::task::Context::from_waker(std::task::Waker::noop()); + if task.as_mut().poll(&mut context).is_pending() { + log::warn!("async source is not immediately ready and no host spawner is wired; the task is dropped"); + } } } diff --git a/node-graph/preprocessor/src/lib.rs b/node-graph/preprocessor/src/lib.rs index 992b16399c..0c1573d172 100644 --- a/node-graph/preprocessor/src/lib.rs +++ b/node-graph/preprocessor/src/lib.rs @@ -131,16 +131,38 @@ impl Preprocessor { input_type = &const { generic!(D) }; } - let inputs: Vec<_> = node_inputs(fields, first_node_io); + let mut inputs: Vec<_> = node_inputs(fields, first_node_io); let input_count = inputs.len(); - let network_inputs = (0..input_count).map(|i| NodeInput::node(NodeId(i as u64), 0)).collect(); + + // The node macro appends a `RuntimeHandle` scope field and a `SourceId` reflection field to every + // async or source kernel. They are resolved inside the substitution network, so the wrapper neither + // exposes them nor carries their context modification. + let injected_field_count = match &fields[..] { + [.., runtime, source] if matches!(runtime.value_source, RegistryValueSource::Scope(_)) && matches!(source.value_source, RegistryValueSource::SourceId) => 2, + _ => 0, + }; + let wrapper_input_count = input_count - injected_field_count; + + inputs.truncate(wrapper_input_count); + + // The injected fields go straight onto the inner node, so the source it reflects is recorded on the + // node that consumes it rather than on a forwarding node that later gets dissolved. + let network_inputs = (0..input_count) + .map(|i| { + if i < wrapper_input_count { + NodeInput::node(NodeId(i as u64), 0) + } else { + injected_field_input(&fields[i]) + } + }) + .collect(); let passthrough_node = ops::passthrough::IDENTIFIER; let mut generated_nodes = 0; let mut nodes: HashMap<_, _, _> = node_io_types .iter() - .take(input_count) + .take(wrapper_input_count) .enumerate() .map(|(i, inputs)| { ( @@ -185,7 +207,7 @@ impl Preprocessor { }) .collect(); - if generated_nodes == 0 && !memoize && !inject_scope { + if generated_nodes == 0 && !memoize && !inject_scope && injected_field_count == 0 { continue; } @@ -199,13 +221,14 @@ impl Preprocessor { ..Default::default() }; - nodes.insert(NodeId(input_count as u64), document_node); + let main_node_id = NodeId(wrapper_input_count as u64); + nodes.insert(main_node_id, document_node); // If memoize is requested, append a Memoize node after the main node and redirect the export through it let export_node_id = if *memoize { - let memoize_node_id = NodeId(input_count as u64 + 1); + let memoize_node_id = NodeId(wrapper_input_count as u64 + 1); let memoize_node = DocumentNode { - inputs: vec![NodeInput::node(NodeId(input_count as u64), 0)], + inputs: vec![NodeInput::node(main_node_id, 0)], implementation: DocumentNodeImplementation::ProtoNode(graphene_core::memo::memoize::IDENTIFIER.clone()), visible: true, ..Default::default() @@ -213,7 +236,7 @@ impl Preprocessor { nodes.insert(memoize_node_id, memoize_node); memoize_node_id } else { - NodeId(input_count as u64) + main_node_id }; let node = DocumentNode { @@ -256,6 +279,14 @@ impl Preprocessor { } } +fn injected_field_input(field: ®istry::FieldMetadata) -> NodeInput { + match field.value_source { + RegistryValueSource::Scope(data) => NodeInput::scope(data), + RegistryValueSource::SourceId => NodeInput::Reflection(DocumentNodeMetadata::SourceId), + _ => NodeInput::value(TaggedValue::None, false), + } +} + pub fn node_inputs(fields: &[registry::FieldMetadata], first_node_io: &NodeIOTypes) -> Vec { fields .iter() diff --git a/test.rs b/test.rs deleted file mode 100644 index 202ae5607d..0000000000 --- a/test.rs +++ /dev/null @@ -1,22 +0,0 @@ -trait Attr { - fn name() -> &'static str -} - -impl Attr for bool { - fn name() {"condition"} -} - -struct Opacity(f64); - -impl Attr for Opacity { - fn name() {"opacity"} -} - - -#[node] -fn opacity(_: impl Ctx, input: T, x: ReadAttr, opacity: WriteAttr) -> T { - if x { - *opacity = 1.; - } - input -}