From a69a8597247524f29d8da7b946ff78b45995a605 Mon Sep 17 00:00:00 2001 From: Timon Date: Mon, 28 Sep 2026 19:52:02 +0000 Subject: [PATCH 01/54] Add editor task module, drop NodeGraphUpdateSender --- editor/src/application.rs | 14 +- editor/src/lib.rs | 1 + .../messages/future/future_message_handler.rs | 104 +++------------ editor/src/messages/future/mod.rs | 2 +- editor/src/messages/prelude.rs | 2 +- editor/src/node_graph_executor.rs | 4 +- editor/src/node_graph_executor/runtime.rs | 43 ++---- editor/src/task.rs | 125 ++++++++++++++++++ node-graph/graphene-cli/src/main.rs | 11 +- .../libraries/application-io/src/lib.rs | 26 ---- 10 files changed, 164 insertions(+), 168 deletions(-) create mode 100644 editor/src/task.rs diff --git a/editor/src/application.rs b/editor/src/application.rs index cd2f2c3f418..643c2aec39f 100644 --- a/editor/src/application.rs +++ b/editor/src/application.rs @@ -1,5 +1,6 @@ use crate::dispatcher::Dispatcher; use crate::messages::prelude::*; +use crate::task::TaskExecutor; use graph_craft::application_io::PlatformApplicationIo; use graph_craft::application_io::resource::ResourceStorage; pub use graphene_std::uuid::*; @@ -7,6 +8,7 @@ use std::sync::{Arc, OnceLock}; pub struct Editor { pub dispatcher: Dispatcher, + task_executor: TaskExecutor, } impl Editor { @@ -21,12 +23,13 @@ impl Editor { ENVIRONMENT.set(environment).expect("Editor shoud only be initialized once"); graphene_std::uuid::set_uuid_seed(uuid_random_seed); + let task_executor = TaskExecutor::new(); let mut dispatcher = Dispatcher::new(resource_storage, document_store); - dispatcher.message_handlers.future_message_handler.set_wake(wake); + dispatcher.message_handlers.future_message_handler = FutureMessageHandler::new(task_executor.handle(), wake); application_io.inject_resource_proxy(dispatcher.message_handlers.resource_storage_message_handler.resources()); crate::node_graph_executor::replace_application_io(application_io); - Self { dispatcher } + Self { dispatcher, task_executor } } #[cfg(test)] @@ -35,9 +38,10 @@ impl Editor { graphene_std::uuid::set_uuid_seed(0); let (mut runtime, executor) = crate::node_graph_executor::NodeGraphExecutor::new_with_local_runtime(); - let editor = Self { - dispatcher: Dispatcher::with_executor(executor), - }; + let task_executor = TaskExecutor::blocking(); + let mut dispatcher = Dispatcher::with_executor(executor); + dispatcher.message_handlers.future_message_handler = FutureMessageHandler::new(task_executor.handle(), Arc::new(|| {})); + let editor = Self { dispatcher, task_executor }; let mut application_io = PlatformApplicationIo::default(); application_io.inject_resource_proxy(editor.dispatcher.message_handlers.resource_storage_message_handler.resources()); diff --git a/editor/src/lib.rs b/editor/src/lib.rs index 56e89e7f4e7..090d06f94ea 100644 --- a/editor/src/lib.rs +++ b/editor/src/lib.rs @@ -15,6 +15,7 @@ pub mod consts; pub mod dispatcher; pub mod messages; pub mod node_graph_executor; +pub mod task; #[cfg(test)] pub mod test_utils; pub mod utility_traits; diff --git a/editor/src/messages/future/future_message_handler.rs b/editor/src/messages/future/future_message_handler.rs index 10731e65efb..49c1c1b7f89 100644 --- a/editor/src/messages/future/future_message_handler.rs +++ b/editor/src/messages/future/future_message_handler.rs @@ -6,13 +6,14 @@ use dyn_any::WasmNotSend; use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender, unbounded}; use crate::messages::prelude::*; +use crate::task::{Spawn, TaskExecutor, TaskExecutorHandle}; #[cfg(not(target_family = "wasm"))] type InnerMessageFuture = Pin + Send + 'static>>; #[cfg(target_family = "wasm")] type InnerMessageFuture = Pin + 'static>>; -/// Invoked by the spawner after a result is sent, to wake the platform event loop. +/// Invoked after a result is sent, to wake the platform event loop. pub type Wake = Arc; fn noop_wake() -> Wake { @@ -62,40 +63,28 @@ where } } -/// Platform-specific async-task executor. -/// Runs `future`, sends the resolved message on `results`, then calls `wake`. -pub trait MessageSpawner: Send + Sync { - fn spawn(&self, future: InnerMessageFuture, results: UnboundedSender, wake: Wake); -} - #[derive(ExtractField)] pub struct FutureMessageContext {} #[derive(ExtractField)] pub struct FutureMessageHandler { - #[cfg_attr(test, expect(dead_code))] - spawner: Arc, + handle: TaskExecutorHandle, wake: Wake, - #[cfg_attr(test, expect(dead_code))] results_sender: UnboundedSender, results_receiver: UnboundedReceiver, } impl FutureMessageHandler { - pub fn with_wake(wake: Wake) -> Self { + pub fn new(handle: TaskExecutorHandle, wake: Wake) -> Self { let (results_sender, results_receiver) = unbounded(); Self { - spawner: default_spawner(), + handle, wake, results_sender, results_receiver, } } - pub fn set_wake(&mut self, wake: Wake) { - self.wake = wake; - } - /// Pull every resolved async result into `out`. pub fn drain_results(&mut self, out: &mut VecDeque) { while let Ok(Some(message)) = self.results_receiver.try_next() { @@ -106,7 +95,7 @@ impl FutureMessageHandler { impl Default for FutureMessageHandler { fn default() -> Self { - Self::with_wake(noop_wake()) + Self::new(TaskExecutor::blocking().handle(), noop_wake()) } } @@ -118,18 +107,17 @@ impl std::fmt::Debug for FutureMessageHandler { #[message_handler_data] impl MessageHandler for FutureMessageHandler { - fn process_message(&mut self, message: FutureMessage, _responses: &mut VecDeque, _context: FutureMessageContext) { + fn process_message(&mut self, message: FutureMessage, responses: &mut VecDeque, _context: FutureMessageContext) { match message { FutureMessage::Await { future } => { - #[cfg(not(test))] - self.spawner.spawn(future.into_future(), self.results_sender.clone(), self.wake.clone()); - - // For tests, block on the future to ensure the result is available when validating editor state afterwards. - #[cfg(test)] - { - let message = futures::executor::block_on(future.into_future()); - _responses.push_back(message); - } + let results = self.results_sender.clone(); + let wake = self.wake.clone(); + self.handle.spawn(Box::pin(async move { + let message = future.await; + let _ = results.unbounded_send(message); + wake(); + })); + self.drain_results(responses); } FutureMessage::Wake => { // Tick-only message: the dispatcher's top-of-tick drain handles the real work. @@ -139,65 +127,3 @@ impl MessageHandler for FutureMessageHandle advertise_actions!(FutureMessageDiscriminant;); } - -#[cfg(not(target_family = "wasm"))] -fn default_spawner() -> Arc { - Arc::new(TokioSpawner::default()) -} - -#[cfg(target_family = "wasm")] -fn default_spawner() -> Arc { - Arc::new(WasmSpawner) -} - -#[cfg(not(target_family = "wasm"))] -struct TokioSpawner { - /// Built lazily on first spawn. `multi_thread(1)` lets Tokio manage its own driver. - runtime: std::sync::OnceLock, -} - -#[cfg(not(target_family = "wasm"))] -impl Default for TokioSpawner { - fn default() -> Self { - Self { runtime: std::sync::OnceLock::new() } - } -} - -#[cfg(not(target_family = "wasm"))] -impl TokioSpawner { - fn runtime(&self) -> &tokio::runtime::Runtime { - self.runtime.get_or_init(|| { - tokio::runtime::Builder::new_multi_thread() - .worker_threads(1) - .thread_name("graphite-async") - .enable_all() - .build() - .expect("failed to construct async-message tokio runtime") - }) - } -} - -#[cfg(not(target_family = "wasm"))] -impl MessageSpawner for TokioSpawner { - fn spawn(&self, future: InnerMessageFuture, results: UnboundedSender, wake: Wake) { - self.runtime().spawn(async move { - let message = future.await; - let _ = results.unbounded_send(message); - wake(); - }); - } -} - -#[cfg(target_family = "wasm")] -struct WasmSpawner; - -#[cfg(target_family = "wasm")] -impl MessageSpawner for WasmSpawner { - fn spawn(&self, future: InnerMessageFuture, results: UnboundedSender, wake: Wake) { - wasm_bindgen_futures::spawn_local(async move { - let message = future.await; - let _ = results.unbounded_send(message); - wake(); - }); - } -} diff --git a/editor/src/messages/future/mod.rs b/editor/src/messages/future/mod.rs index 912d28c29fc..20981f5d09e 100644 --- a/editor/src/messages/future/mod.rs +++ b/editor/src/messages/future/mod.rs @@ -4,4 +4,4 @@ mod future_message_handler; #[doc(inline)] pub use future_message::{FutureMessage, FutureMessageDiscriminant}; #[doc(inline)] -pub use future_message_handler::{FutureMessageContext, FutureMessageHandler, MessageFuture, MessageSpawner, Wake}; +pub use future_message_handler::{FutureMessageContext, FutureMessageHandler, MessageFuture, Wake}; diff --git a/editor/src/messages/prelude.rs b/editor/src/messages/prelude.rs index e47e66e7284..e183c1dfb90 100644 --- a/editor/src/messages/prelude.rs +++ b/editor/src/messages/prelude.rs @@ -16,7 +16,7 @@ pub use crate::messages::dialog::new_document_dialog::{NewDocumentDialogMessage, pub use crate::messages::dialog::preferences_dialog::{PreferencesDialogMessage, PreferencesDialogMessageContext, PreferencesDialogMessageDiscriminant, PreferencesDialogMessageHandler}; pub use crate::messages::dialog::{DialogMessage, DialogMessageContext, DialogMessageDiscriminant, DialogMessageHandler}; pub use crate::messages::frontend::{FrontendMessage, FrontendMessageDiscriminant}; -pub use crate::messages::future::{FutureMessage, FutureMessageContext, FutureMessageDiscriminant, FutureMessageHandler, MessageFuture, MessageSpawner, Wake}; +pub use crate::messages::future::{FutureMessage, FutureMessageContext, FutureMessageDiscriminant, FutureMessageHandler, MessageFuture, Wake}; pub use crate::messages::input_mapper::key_mapping::{KeyMappingMessage, KeyMappingMessageContext, KeyMappingMessageDiscriminant, KeyMappingMessageHandler}; pub use crate::messages::input_mapper::{InputMapperMessage, InputMapperMessageContext, InputMapperMessageDiscriminant, InputMapperMessageHandler}; pub use crate::messages::input_preprocessor::{InputPreprocessorMessage, InputPreprocessorMessageContext, InputPreprocessorMessageDiscriminant, InputPreprocessorMessageHandler}; diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index c50879aa562..ec8f6481c2d 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -6,7 +6,7 @@ use graph_craft::application_io::EditorPreferences; use graph_craft::document::value::{RenderOutput, RenderOutputType, TaggedValue}; use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput, NodeNetwork}; use graph_craft::proto::GraphErrors; -use graphene_std::application_io::{ExportFormat, NodeGraphUpdateMessage, RenderConfig, TimingInformation}; +use graphene_std::application_io::{ExportFormat, RenderConfig, TimingInformation}; use graphene_std::bounds::RenderBoundingBox; use graphene_std::color::SRGBA8; use graphene_std::list::List; @@ -51,7 +51,6 @@ pub enum NodeGraphUpdate { ExecutionResponse(Box), CompilationResponse(CompilationResponse), EyedropperPreview(Raster), - NodeGraphUpdateMessage(NodeGraphUpdateMessage), } #[derive(Debug, Default)] @@ -465,7 +464,6 @@ impl NodeGraphExecutor { let (data, width, height) = raster.to_flat_u8(); responses.add(EyedropperToolMessage::PreviewImage { data, width, height }); } - NodeGraphUpdate::NodeGraphUpdateMessage(_) => {} } } diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index 8ce1ccea575..50832f2b97f 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -8,7 +8,7 @@ use graph_craft::document::value::{RenderOutput, RenderOutputType, TaggedValue}; use graph_craft::document::{NodeId, NodeNetwork}; use graph_craft::graphene_compiler::Compiler; use graph_craft::proto::GraphErrors; -use graphene_std::application_io::{ApplicationIo, ExportFormat, NodeGraphUpdateMessage, NodeGraphUpdateSender, RenderConfig, Texture}; +use graphene_std::application_io::{ApplicationIo, ExportFormat, RenderConfig, Texture}; use graphene_std::bounds::RenderBoundingBox; use graphene_std::list::{Item, List}; use graphene_std::memo::IORecord; @@ -36,7 +36,7 @@ pub struct NodeRuntime { #[cfg(not(test))] executor: DynamicExecutor, receiver: Receiver, - sender: InternalNodeGraphUpdateSender, + sender: Sender, editor_preferences: EditorPreferences, old_graph: Option, update_thumbnails: bool, @@ -93,29 +93,6 @@ pub struct ExportConfig { pub artboard_count: usize, } -#[derive(Clone)] -struct InternalNodeGraphUpdateSender(Sender); - -impl InternalNodeGraphUpdateSender { - fn send_compilation_response(&self, response: CompilationResponse) { - self.0.send(NodeGraphUpdate::CompilationResponse(response)).expect("Failed to send response") - } - - fn send_execution_response(&self, response: ExecutionResponse) { - self.0.send(NodeGraphUpdate::ExecutionResponse(Box::new(response))).expect("Failed to send response") - } - - fn send_eyedropper_preview(&self, raster: Raster) { - self.0.send(NodeGraphUpdate::EyedropperPreview(raster)).expect("Failed to send response") - } -} - -impl NodeGraphUpdateSender for InternalNodeGraphUpdateSender { - fn send(&self, message: NodeGraphUpdateMessage) { - self.0.send(NodeGraphUpdate::NodeGraphUpdateMessage(message)).expect("Failed to send response") - } -} - // TODO: Replace with `core::cell::LazyCell` () or similar pub static NODE_RUNTIME: once_cell::sync::Lazy>> = once_cell::sync::Lazy::new(|| Mutex::new(None)); @@ -124,7 +101,7 @@ impl NodeRuntime { Self { executor: DynamicExecutor::default(), receiver, - sender: InternalNodeGraphUpdateSender(sender.clone()), + sender, editor_preferences: EditorPreferences::default(), old_graph: None, resources: ResourceRegistry::default(), @@ -132,7 +109,6 @@ impl NodeRuntime { editor_api: PlatformEditorApi { editor_preferences: Box::new(EditorPreferences::default()), - node_graph_message_sender: Box::new(InternalNodeGraphUpdateSender(sender)), #[cfg(not(test))] application_io: None, @@ -201,7 +177,6 @@ impl NodeRuntime { self.editor_preferences = preferences.clone(); self.editor_api = PlatformEditorApi { application_io: self.editor_api.application_io.clone(), - node_graph_message_sender: Box::new(self.sender.clone()), editor_preferences: Box::new(preferences), } .into(); @@ -227,7 +202,9 @@ impl NodeRuntime { self.update_thumbnails = true; - self.sender.send_compilation_response(CompilationResponse { result, node_graph_errors }); + self.sender + .send(NodeGraphUpdate::CompilationResponse(CompilationResponse { result, node_graph_errors })) + .expect("Failed to send response"); } GraphRuntimeRequest::ExecutionRequest(ExecutionRequest { execution_id, mut render_config, .. }) => { // We may want to render via the SVG pipeline even though raster was requested, if SVG Preview render mode is active or WebGPU/Vello is unavailable @@ -287,7 +264,7 @@ impl NodeRuntime { let raster_cpu = Raster::new_gpu(texture).convert(Footprint::BOUNDLESS, executor).await; - self.sender.send_eyedropper_preview(raster_cpu); + self.sender.send(NodeGraphUpdate::EyedropperPreview(raster_cpu)).expect("Failed to send response"); continue; } // Eyedropper render that didn't produce a texture (e.g., SVG fallback when GPU is unavailable); discard it @@ -331,13 +308,14 @@ impl NodeRuntime { r => (r, None), }; - self.sender.send_execution_response(ExecutionResponse { + let response = ExecutionResponse { execution_id, result, responses, vector_modify: self.vector_modify.clone(), inspect_result, - }); + }; + self.sender.send(NodeGraphUpdate::ExecutionResponse(Box::new(response))).expect("Failed to send response"); return texture; } } @@ -597,7 +575,6 @@ impl NodeRuntime { pub(crate) fn replace_application_io(&mut self, application_io: PlatformApplicationIo) { self.editor_api = PlatformEditorApi { application_io: Some(application_io.into()), - node_graph_message_sender: Box::new(self.sender.clone()), editor_preferences: Box::new(self.editor_preferences.clone()), } .into(); diff --git a/editor/src/task.rs b/editor/src/task.rs new file mode 100644 index 00000000000..d37d5d52fff --- /dev/null +++ b/editor/src/task.rs @@ -0,0 +1,125 @@ +use std::pin::Pin; +use std::sync::Arc; + +#[cfg(not(target_family = "wasm"))] +pub type Task = Pin + Send + 'static>>; +#[cfg(target_family = "wasm")] +pub type Task = Pin + 'static>>; + +pub trait Spawn { + fn spawn(&self, task: Task); +} + +#[derive(Clone)] +pub struct TaskExecutorHandle { + spawner: Arc, +} + +impl Spawn for TaskExecutorHandle { + fn spawn(&self, task: Task) { + self.spawner.spawn(task); + } +} + +pub struct TaskExecutor { + spawner: Arc, +} + +impl TaskExecutor { + pub fn new() -> Self { + let spawner = { + #[cfg(target_family = "wasm")] + { + Arc::new(Spawner::Wasm(WasmSpawner)) + } + #[cfg(not(target_family = "wasm"))] + { + Arc::new(Spawner::Tokio(TokioSpawner::new())) + } + }; + Self { spawner } + } + pub fn blocking() -> Self { + let spawner = Arc::new(Spawner::Blocking(BlockingSpawner)); + Self { spawner } + } + pub fn handle(&self) -> TaskExecutorHandle { + TaskExecutorHandle { spawner: self.spawner.clone() } + } +} + +impl Default for TaskExecutor { + fn default() -> Self { + Self::new() + } +} + +enum Spawner { + #[cfg(target_family = "wasm")] + Wasm(WasmSpawner), + #[cfg(not(target_family = "wasm"))] + Tokio(TokioSpawner), + Blocking(BlockingSpawner), +} + +impl Spawn for Spawner { + fn spawn(&self, task: Task) { + match self { + #[cfg(target_family = "wasm")] + Spawner::Wasm(spawner) => spawner.spawn(task), + #[cfg(not(target_family = "wasm"))] + Spawner::Tokio(spawner) => spawner.spawn(task), + Spawner::Blocking(spawner) => spawner.spawn(task), + } + } +} + +#[cfg(target_family = "wasm")] +struct WasmSpawner; + +#[cfg(target_family = "wasm")] +impl Spawn for WasmSpawner { + fn spawn(&self, task: Task) { + wasm_bindgen_futures::spawn_local(task); + } +} + +#[cfg(not(target_family = "wasm"))] +struct TokioSpawner(Option); + +#[cfg(not(target_family = "wasm"))] +impl TokioSpawner { + fn new() -> Self { + let runtime = tokio::runtime::Builder::new_multi_thread() + .worker_threads(1) + .thread_name("graphite-async") + .enable_all() + .build() + .expect("failed to construct the task tokio runtime"); + Self(Some(runtime)) + } +} + +#[cfg(not(target_family = "wasm"))] +impl Spawn for TokioSpawner { + fn spawn(&self, task: Task) { + self.0.as_ref().expect("the runtime lives until drop").spawn(task); + } +} + +#[cfg(not(target_family = "wasm"))] +impl Drop for TokioSpawner { + fn drop(&mut self) { + if let Some(runtime) = self.0.take() { + runtime.shutdown_background(); + } + } +} + +struct BlockingSpawner; + +impl Spawn for BlockingSpawner { + fn spawn(&self, task: Task) { + futures::executor::block_on(task); + } +} diff --git a/node-graph/graphene-cli/src/main.rs b/node-graph/graphene-cli/src/main.rs index 765c96f6c06..756419fae93 100644 --- a/node-graph/graphene-cli/src/main.rs +++ b/node-graph/graphene-cli/src/main.rs @@ -12,21 +12,13 @@ use graph_craft::document::*; use graph_craft::graphene_compiler::Compiler; use graph_craft::proto::ProtoNetwork; use graph_craft::util::load_network; -use graphene_std::application_io::{ApplicationIo, NodeGraphUpdateMessage, NodeGraphUpdateSender}; +use graphene_std::application_io::ApplicationIo; use interpreted_executor::dynamic_executor::DynamicExecutor; use interpreted_executor::util::wrap_network_in_scope; use std::error::Error; use std::path::PathBuf; use std::sync::Arc; -struct UpdateLogger {} - -impl NodeGraphUpdateSender for UpdateLogger { - fn send(&self, message: NodeGraphUpdateMessage) { - println!("{message:?}"); - } -} - #[derive(Debug, Parser)] #[clap(name = "graphene-cli", version)] pub struct App { @@ -183,7 +175,6 @@ async fn main() -> Result<(), Box> { }; let editor_api = Arc::new(PlatformEditorApi { application_io: Some(application_io_for_api), - node_graph_message_sender: Box::new(UpdateLogger {}), editor_preferences: Box::new(preferences), }); let proto_graph = compile_graph(node_network, editor_api, gdd.as_ref())?; diff --git a/node-graph/libraries/application-io/src/lib.rs b/node-graph/libraries/application-io/src/lib.rs index b3df3314d31..3a6032e6df7 100644 --- a/node-graph/libraries/application-io/src/lib.rs +++ b/node-graph/libraries/application-io/src/lib.rs @@ -38,20 +38,6 @@ impl ApplicationIo for &T { } } -#[derive(Debug, Clone)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub enum NodeGraphUpdateMessage {} - -pub trait NodeGraphUpdateSender { - fn send(&self, message: NodeGraphUpdateMessage); -} - -impl NodeGraphUpdateSender for std::sync::Mutex { - fn send(&self, message: NodeGraphUpdateMessage) { - self.lock().as_mut().unwrap().send(message) - } -} - pub trait GetEditorPreferences { fn max_render_region_area(&self) -> u32; } @@ -92,14 +78,6 @@ impl RenderConfig { } } -struct Logger; - -impl NodeGraphUpdateSender for Logger { - fn send(&self, message: NodeGraphUpdateMessage) { - log::warn!("dispatching message with fallback node graph update sender {message:?}"); - } -} - struct DummyPreferences; impl GetEditorPreferences for DummyPreferences { @@ -111,7 +89,6 @@ impl GetEditorPreferences for DummyPreferences { pub struct EditorApi { /// Gives access to APIs like resources. pub application_io: Option>, - pub node_graph_message_sender: Box, /// Editor preferences made available to the graph through the `PlatformEditorApi`. pub editor_preferences: Box, } @@ -122,7 +99,6 @@ impl Default for EditorApi { fn default() -> Self { Self { application_io: None, - node_graph_message_sender: Box::new(Logger), editor_preferences: Box::new(DummyPreferences), } } @@ -131,7 +107,6 @@ impl Default for EditorApi { impl Hash for EditorApi { fn hash(&self, state: &mut H) { self.application_io.as_ref().map_or(0, |io| io as *const _ as usize).hash(state); - (self.node_graph_message_sender.as_ref() as *const dyn NodeGraphUpdateSender).hash(state); (self.editor_preferences.as_ref() as *const dyn GetEditorPreferences).hash(state); } } @@ -145,7 +120,6 @@ impl CacheHash for EditorApi { impl PartialEq for EditorApi { fn eq(&self, other: &Self) -> bool { self.application_io.as_ref().map_or(0, |io| addr_of!(io) as usize) == other.application_io.as_ref().map_or(0, |io| addr_of!(io) as usize) - && std::ptr::eq(self.node_graph_message_sender.as_ref() as *const _, other.node_graph_message_sender.as_ref() as *const _) && std::ptr::eq(self.editor_preferences.as_ref() as *const _, other.editor_preferences.as_ref() as *const _) } } From 0695cf177f91c2d5d5dfc7174c32e861084b8398 Mon Sep 17 00:00:00 2001 From: Timon Date: Tue, 29 Sep 2026 13:05:42 +0000 Subject: [PATCH 02/54] Remove Node::reset and unused value nodes --- node-graph/README.md | 4 +- node-graph/libraries/core-types/src/lib.rs | 2 - node-graph/libraries/core-types/src/ops.rs | 4 - .../libraries/core-types/src/registry.rs | 11 -- node-graph/libraries/core-types/src/value.rs | 187 ------------------ 5 files changed, 2 insertions(+), 206 deletions(-) diff --git a/node-graph/README.md b/node-graph/README.md index 39025259b9b..19edcc5ea67 100644 --- a/node-graph/README.md +++ b/node-graph/README.md @@ -73,13 +73,13 @@ To test this: #[test] fn test_opacity_node() { let opacity_node = OpacityNode { - opacity_multiplier: crate::value::CopiedNode(10_f64), // set opacity to 10% + opacity_multiplier: crate::value::ClonedNode(10_f64), // set opacity to 10% }; assert_eq!(opacity_node.eval(Color::WHITE), Color::from_rgbaf32_unchecked(1., 1., 1., 0.1)); } ``` -The `graphene_core::value::CopiedNode` is a node that, when evaluated, copies `10_f64` and returns it. +The `graphene_core::value::ClonedNode` is a node that, when evaluated, clones `10_f64` and returns it. ## Creating a new node diff --git a/node-graph/libraries/core-types/src/lib.rs b/node-graph/libraries/core-types/src/lib.rs index 5de86b6f89f..73b02bb8a77 100644 --- a/node-graph/libraries/core-types/src/lib.rs +++ b/node-graph/libraries/core-types/src/lib.rs @@ -53,8 +53,6 @@ pub trait Node<'i, Input> { type Output: 'i; /// Evaluates the node with the single specified input. fn eval(&'i self, input: Input) -> Self::Output; - /// Resets the node, e.g. the LetNode's cache is set to None. - fn reset(&self) {} /// Returns the name of the node for diagnostic purposes. fn node_name(&self) -> &'static str { std::any::type_name::() diff --git a/node-graph/libraries/core-types/src/ops.rs b/node-graph/libraries/core-types/src/ops.rs index 3fb436b015b..17db5a7f70b 100644 --- a/node-graph/libraries/core-types/src/ops.rs +++ b/node-graph/libraries/core-types/src/ops.rs @@ -18,10 +18,6 @@ where self.0.eval(input) } - fn reset(&self) { - self.0.reset(); - } - fn serialize(&self) -> Option> { self.0.serialize() } diff --git a/node-graph/libraries/core-types/src/registry.rs b/node-graph/libraries/core-types/src/registry.rs index 65f9a4394f7..2026aaff6d0 100644 --- a/node-graph/libraries/core-types/src/registry.rs +++ b/node-graph/libraries/core-types/src/registry.rs @@ -177,9 +177,6 @@ where }) } } - fn reset(&self) { - self.node.reset(); - } fn serialize(&self) -> Option> { self.node.serialize() @@ -208,10 +205,6 @@ where let result = self.node.eval(input); Box::pin(async move { result }) } - #[inline(always)] - fn reset(&self) { - self.node.reset(); - } #[inline(always)] fn serialize(&self) -> Option> { @@ -251,10 +244,6 @@ where } } - fn reset(&self) { - self.node.reset(); - } - fn serialize(&self) -> Option> { self.node.serialize() } diff --git a/node-graph/libraries/core-types/src/value.rs b/node-graph/libraries/core-types/src/value.rs index 51e9d5490cc..4f847012be5 100644 --- a/node-graph/libraries/core-types/src/value.rs +++ b/node-graph/libraries/core-types/src/value.rs @@ -1,93 +1,6 @@ use crate::list::Item; use crate::registry::DynFuture; use crate::{Node, WasmNotSend}; -use std::cell::{Cell, RefCell, RefMut}; -use std::marker::PhantomData; - -#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] -pub struct IntNode; - -impl<'i, const N: u32, I> Node<'i, I> for IntNode { - type Output = u32; - #[inline(always)] - fn eval(&'i self, _input: I) -> Self::Output { - N - } -} - -#[derive(Default, Debug, Clone, Copy)] -pub struct ValueNode(pub T); - -impl<'i, T: 'i, I> Node<'i, I> for ValueNode { - type Output = &'i T; - #[inline(always)] - fn eval(&'i self, _input: I) -> Self::Output { - &self.0 - } -} - -impl ValueNode { - pub const fn new(value: T) -> ValueNode { - ValueNode(value) - } -} - -impl From for ValueNode { - fn from(value: T) -> Self { - ValueNode::new(value) - } -} - -#[derive(Default, Debug, Clone, Copy)] -pub struct AsRefNode, U>(pub T, PhantomData); - -impl<'i, T: 'i + AsRef, U: 'i> Node<'i, ()> for AsRefNode { - type Output = &'i U; - #[inline(always)] - fn eval(&'i self, _input: ()) -> Self::Output { - self.0.as_ref() - } -} - -impl, U> AsRefNode { - pub const fn new(value: T) -> AsRefNode { - AsRefNode(value, PhantomData) - } -} - -#[derive(Default, Debug, Clone)] -pub struct RefCellMutNode(pub RefCell); - -impl<'i, T: 'i> Node<'i, ()> for RefCellMutNode { - type Output = RefMut<'i, T>; - #[inline(always)] - fn eval(&'i self, _input: ()) -> Self::Output { - self.0.borrow_mut() - } -} - -impl RefCellMutNode { - pub const fn new(value: T) -> RefCellMutNode { - RefCellMutNode(RefCell::new(value)) - } -} - -#[derive(Default)] -pub struct OnceCellNode(pub Cell); - -impl<'i, T: Default + 'i, I> Node<'i, I> for OnceCellNode { - type Output = T; - #[inline(always)] - fn eval(&'i self, _input: I) -> Self::Output { - self.0.replace(T::default()) - } -} - -impl OnceCellNode { - pub const fn new(value: T) -> OnceCellNode { - OnceCellNode(Cell::new(value)) - } -} #[derive(Clone, Copy)] pub struct ClonedNode(pub T); @@ -131,103 +44,3 @@ impl PrecomputedItemNode { Self(item) } } - -#[derive(Clone, Copy)] -/// The DebugClonedNode logs every time it is evaluated. -/// This is useful for debugging. -pub struct DebugClonedNode(pub T); - -impl<'i, T: Clone + 'i> Node<'i, ()> for DebugClonedNode { - type Output = T; - #[inline(always)] - fn eval(&'i self, _input: ()) -> Self::Output { - // KEEP THIS `debug!()` - It acts as the output for the debug node itself - log::debug!("DebugClonedNode::eval"); - - self.0.clone() - } -} - -impl DebugClonedNode { - pub const fn new(value: T) -> DebugClonedNode { - DebugClonedNode(value) - } -} - -#[derive(Clone, Copy)] -pub struct CopiedNode(pub T); - -impl<'i, T: Copy + 'i, I> Node<'i, I> for CopiedNode { - type Output = T; - #[inline(always)] - fn eval(&'i self, _input: I) -> Self::Output { - self.0 - } -} - -impl CopiedNode { - pub const fn new(value: T) -> CopiedNode { - CopiedNode(value) - } -} - -#[derive(Default)] -pub struct DefaultNode(PhantomData); - -impl<'i, T: Default + 'i, I> Node<'i, I> for DefaultNode { - type Output = T; - fn eval(&'i self, _input: I) -> Self::Output { - T::default() - } -} - -impl DefaultNode { - pub fn new() -> Self { - Self(PhantomData) - } -} - -#[repr(C)] -/// Return the unit value -#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] -pub struct ForgetNode; - -impl<'i, T: 'i> Node<'i, T> for ForgetNode { - type Output = (); - fn eval(&'i self, _input: T) -> Self::Output {} -} - -impl ForgetNode { - pub const fn new() -> Self { - ForgetNode - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_int_node() { - let node = IntNode::<5>; - assert_eq!(node.eval(()), 5); - } - #[test] - fn test_value_node() { - let node = ValueNode::new(5); - assert_eq!(node.eval(()), &5); - let type_erased = &node as &dyn for<'a> Node<'a, (), Output = &'a i32>; - assert_eq!(type_erased.eval(()), &5); - } - #[test] - fn test_default_node() { - let node = DefaultNode::::new(); - assert_eq!(node.eval(42), 0); - } - #[test] - #[allow(clippy::unit_cmp)] - fn test_unit_node() { - let node = ForgetNode::new(); - assert_eq!(node.eval(()), ()); - } -} From 2ecccf2df11bd222f5789aa57069285e905a52b6 Mon Sep 17 00:00:00 2001 From: Timon Date: Tue, 29 Sep 2026 13:07:13 +0000 Subject: [PATCH 03/54] Remove unneeded async from vector nodes --- node-graph/nodes/repeat/src/repeat_nodes.rs | 8 +- .../vector/src/vector_modification_nodes.rs | 4 +- node-graph/nodes/vector/src/vector_nodes.rs | 124 ++++++++---------- 3 files changed, 64 insertions(+), 72 deletions(-) diff --git a/node-graph/nodes/repeat/src/repeat_nodes.rs b/node-graph/nodes/repeat/src/repeat_nodes.rs index 370f2059eb7..93d07ae40ee 100644 --- a/node-graph/nodes/repeat/src/repeat_nodes.rs +++ b/node-graph/nodes/repeat/src/repeat_nodes.rs @@ -323,7 +323,7 @@ mod test { Item::new_from_element(count), ) .await; - let vector_list = List::new_from_item(vector_nodes::combine_paths(Footprint::default(), List::new_from_element(Graphic::VectorList(repeated))).await); + let vector_list = List::new_from_item(vector_nodes::combine_paths(Footprint::default(), List::new_from_element(Graphic::VectorList(repeated)))); let vector = vector_list.element(0).unwrap(); assert_eq!(vector.stroke_manipulator_groups().count(), 3); for (index, (manipulator_groups, _)) in vector.stroke_manipulator_groups().enumerate() { @@ -342,7 +342,7 @@ mod test { Item::new_from_element(1), ) .await; - let vector_list = List::new_from_item(vector_nodes::combine_paths(Footprint::default(), List::new_from_element(Graphic::VectorList(repeated))).await); + let vector_list = List::new_from_item(vector_nodes::combine_paths(Footprint::default(), List::new_from_element(Graphic::VectorList(repeated)))); let vector = vector_list.element(0).unwrap(); assert_eq!(vector.stroke_manipulator_groups().count(), 1); @@ -364,7 +364,7 @@ mod test { Item::new_from_element(count), ) .await; - let vector_list = List::new_from_item(vector_nodes::combine_paths(Footprint::default(), List::new_from_element(Graphic::VectorList(repeated))).await); + let vector_list = List::new_from_item(vector_nodes::combine_paths(Footprint::default(), List::new_from_element(Graphic::VectorList(repeated)))); let vector = vector_list.element(0).unwrap(); assert_eq!(vector.stroke_manipulator_groups().count(), 8); for (index, (manipulator_groups, _)) in vector.stroke_manipulator_groups().enumerate() { @@ -383,7 +383,7 @@ mod test { Item::new_from_element(8), ) .await; - let vector_list = List::new_from_item(vector_nodes::combine_paths(Footprint::default(), List::new_from_element(Graphic::VectorList(repeated))).await); + let vector_list = List::new_from_item(vector_nodes::combine_paths(Footprint::default(), List::new_from_element(Graphic::VectorList(repeated)))); let vector = vector_list.element(0).unwrap(); assert_eq!(vector.stroke_manipulator_groups().count(), 8); diff --git a/node-graph/nodes/vector/src/vector_modification_nodes.rs b/node-graph/nodes/vector/src/vector_modification_nodes.rs index 35422566033..9a78fe7fc4b 100644 --- a/node-graph/nodes/vector/src/vector_modification_nodes.rs +++ b/node-graph/nodes/vector/src/vector_modification_nodes.rs @@ -8,7 +8,7 @@ use vector_types::vector::VectorModification; /// Applies a differential modification to a vector path, associating changes made by the Pen and Path tools to indices of edited points and segments. #[node_macro::node(category(""))] -async fn path_modify(_ctx: impl Ctx, vector: Item, modification: Item>, node_path: Item) -> Item { +fn path_modify(_ctx: impl Ctx, vector: Item, modification: Item>, node_path: Item) -> Item { let mut vector = vector; modification.into_element().apply(vector.element_mut()); @@ -35,7 +35,7 @@ async fn path_modify(_ctx: impl Ctx, vector: Item, modification: Item(_ctx: impl Ctx, #[implementations(Vector, DAffine2, DVec2)] content: Item) -> Item { +fn bake_transform(_ctx: impl Ctx, #[implementations(Vector, DAffine2, DVec2)] content: Item) -> Item { let mut content = content; if let Some(transform) = content.remove_attribute::(ATTR_TRANSFORM) { content.element_mut().bake_transform(&transform); diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index 0f13886b921..2604469b240 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -215,7 +215,7 @@ impl ExpandVectorItems for Graphic { /// Uniquely sets the fill and/or stroke style of every vector element to individual colors sampled along a chosen gradient. #[node_macro::node(category("Vector: Style"), path(graphene_core::vector))] -async fn assign_colors( +fn assign_colors( _: impl Ctx, /// The content with vector paths to apply the fill and/or stroke style to. #[implementations(List, List)] @@ -304,7 +304,7 @@ where /// Applies a fill style to the vector content, giving an appearance to the area within the interior of the geometry. #[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("fill_properties"))] -async fn fill( +fn fill( _: impl Ctx, /// The content with vector paths to apply the fill style to. #[implementations(Vector, Graphic)] @@ -394,7 +394,7 @@ where /// Applies a stroke style to the vector content, giving an appearance to the area within the outline of the geometry. #[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("stroke_properties"))] -async fn stroke( +fn stroke( _: impl Ctx, /// The content with vector paths to apply the stroke style to. #[implementations(Vector, Graphic)] @@ -473,7 +473,7 @@ fn dash_pattern( } #[node_macro::node(category("Repeat"), name("Copy to Points"), path(core_types::vector))] -async fn copy_to_points( +fn copy_to_points( _: impl Ctx, points: List, /// Artwork to be copied and placed at each point. @@ -563,7 +563,7 @@ async fn copy_to_points( } #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] -async fn round_corners( +fn round_corners( _: impl Ctx, #[implementations(Graphic, Vector)] source: Item, #[unit(" px")] @@ -886,7 +886,7 @@ pub mod extrude_algorithms { } #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] -async fn extrude(_: impl Ctx, #[implementations(Graphic, Vector)] source: Item, direction: Item, joining_algorithm: Item) -> Item { +fn extrude(_: impl Ctx, #[implementations(Graphic, Vector)] source: Item, direction: Item, joining_algorithm: Item) -> Item { V::map_vector_items(source, |source| { let mut source = source; let (direction, joining_algorithm) = (*direction.element(), *joining_algorithm.element()); @@ -897,7 +897,7 @@ async fn extrude(_: impl Ctx, #[implementations(G } #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] -async fn box_warp(_: impl Ctx, #[implementations(Graphic, Vector)] content: Item, #[expose] rectangle: Item) -> Item { +fn box_warp(_: impl Ctx, #[implementations(Graphic, Vector)] content: Item, #[expose] rectangle: Item) -> Item { let target_transform: DAffine2 = rectangle.attribute_cloned_or_default(ATTR_TRANSFORM); let target = rectangle.into_element(); @@ -980,7 +980,7 @@ fn bilinear_interpolate(t: DVec2, quad: &[DVec2; 4]) -> DVec2 { } #[node_macro::node(category("Vector"), path(graphene_core::vector))] -async fn pack_strips( +fn pack_strips( _: impl Ctx, #[implementations( List, @@ -1103,7 +1103,7 @@ where /// Automatically constructs tangents (Bézier handles) for anchor points in a vector path. #[node_macro::node(category("Vector: Modifier"), name("Auto-Tangents"), path(core_types::vector))] -async fn auto_tangents( +fn auto_tangents( _: impl Ctx, #[implementations(Graphic, Vector)] source: Item, /// The amount of spread for the auto-tangents, from 0 (sharp corner) to 1 (full spread). @@ -1255,7 +1255,7 @@ async fn auto_tangents( } #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] -async fn bounding_box(_: impl Ctx, #[implementations(Graphic, Vector)] content: Item) -> Item { +fn bounding_box(_: impl Ctx, #[implementations(Graphic, Vector)] content: Item) -> Item { V::map_vector_items(content, |content| { let mut content = content; let result = content @@ -1274,7 +1274,7 @@ async fn bounding_box(_: impl Ctx, #[implementati } #[node_macro::node(category("Vector: Measure"), path(core_types::vector))] -async fn dimensions(_: impl Ctx, content: Item) -> Item { +fn dimensions(_: impl Ctx, content: Item) -> Item { let dimensions = content .element() .bounding_box_with_transform(content.attribute_cloned_or_default(ATTR_TRANSFORM)) @@ -1308,7 +1308,7 @@ fn points_to_vector( /// Creates a polyline from a series of vector points, replacing any existing segments that may already exist. #[node_macro::node(category("Vector"), name("Points to Polyline"), path(core_types::vector))] -async fn points_to_polyline(_: impl Ctx, #[implementations(Graphic, Vector)] points: Item, #[default(true)] closed: Item) -> Item { +fn points_to_polyline(_: impl Ctx, #[implementations(Graphic, Vector)] points: Item, #[default(true)] closed: Item) -> Item { let closed = *closed.element(); V::map_vector_items(points, |points| { @@ -1338,7 +1338,7 @@ async fn points_to_polyline(_: impl Ctx, #[implem /// Evens out the distances between points by applying Lloyd's relaxation, moving every interior point toward the center of its Voronoi cell. #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] -async fn relax_points( +fn relax_points( _: impl Ctx, /// A vector path or point cloud to relax. #[implementations(Graphic, Vector)] @@ -1366,7 +1366,7 @@ async fn relax_points( /// /// When Connect Cells is off, every cell becomes its own closed, fillable subpath. When on, the cells share their common points and segments, forming a single connected mesh. #[node_macro::node(category("Vector"), path(core_types::vector))] -async fn voronoi_cells(_: impl Ctx, #[implementations(Graphic, Vector)] source: Item, connect_cells: Item) -> Item { +fn voronoi_cells(_: impl Ctx, #[implementations(Graphic, Vector)] source: Item, connect_cells: Item) -> Item { V::map_vector_items(source, |source| { let mut source = source; let connect_cells = *connect_cells.element(); @@ -1386,7 +1386,7 @@ async fn voronoi_cells(_: impl Ctx, #[implementat /// /// When Connect Cells is off, every triangle becomes its own closed, fillable subpath. When on, the triangles share their common points and segments, forming a single connected mesh. #[node_macro::node(category("Vector"), path(core_types::vector))] -async fn triangulate(_: impl Ctx, #[implementations(Graphic, Vector)] source: Item, connect_cells: Item) -> Item { +fn triangulate(_: impl Ctx, #[implementations(Graphic, Vector)] source: Item, connect_cells: Item) -> Item { V::map_vector_items(source, |source| { let mut source = source; let connect_cells = *connect_cells.element(); @@ -1496,7 +1496,7 @@ fn mesh_weld_tolerance(polygons: &[Vec]) -> f64 { } #[node_macro::node(category("Vector: Modifier"), path(core_types::vector), properties("offset_path_properties"))] -async fn offset_path( +fn offset_path( _: impl Ctx, #[implementations(Graphic, Vector)] content: Item, distance: Item, @@ -1699,14 +1699,14 @@ fn solidify_stroke_list(content: List) -> List { } #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] -async fn solidify_stroke(_: impl Ctx, #[implementations(Graphic, Vector)] content: Item) -> List { +fn solidify_stroke(_: impl Ctx, #[implementations(Graphic, Vector)] content: Item) -> List { // TODO: Make this node support stroke align, which it currently ignores V::solidify_strokes(content) } #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] -async fn separate_subpaths(_: impl Ctx, #[implementations(Graphic, Vector)] content: Item) -> List { +fn separate_subpaths(_: impl Ctx, #[implementations(Graphic, Vector)] content: Item) -> List { V::expand_vector_items(content, |content| { let bezpaths = content.element().stroke_bezpath_iter().collect::>(); @@ -1732,7 +1732,7 @@ async fn separate_subpaths(_: impl Ctx, #[impl /// Determines if the subpath at the given index is closed, meaning its ends are connected together forming a loop. #[node_macro::node(name("Path is Closed"), category("Vector: Measure"), path(core_types::vector))] -async fn path_is_closed( +fn path_is_closed( _: impl Ctx, /// The vector content whose subpaths are inspected. content: Item, @@ -1767,7 +1767,7 @@ async fn map_points( /// Combines every vector path across the input into a single compound path. #[node_macro::node(category("Vector"), path(graphene_core::vector))] -pub async fn combine_paths(_: impl Ctx, content: List) -> Item { +pub fn combine_paths(_: impl Ctx, content: List) -> Item { let graphic_list = content.clone(); let flattened = content.into_flattened_list::(); @@ -1816,7 +1816,7 @@ pub async fn combine_paths(_: impl Ctx, content: List) -> Item /// Convert vector geometry into a polyline composed of evenly spaced points. #[node_macro::node(category("Vector: Modifier"), path(core_types::vector), properties("sample_polyline_properties"), memoize)] -async fn sample_polyline( +fn sample_polyline( _: impl Ctx, #[implementations(Graphic, Vector)] content: Item, spacing: Item, @@ -1895,7 +1895,7 @@ async fn sample_polyline( /// Simplifies vector paths by reducing the number of curve segments while preserving the overall shape within the given tolerance. #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] -async fn simplify( +fn simplify( _: impl Ctx, /// The vector paths to simplify. #[implementations(Graphic, Vector)] @@ -1937,7 +1937,7 @@ async fn simplify( /// Decimates vector paths into polylines by sampling any curves into line segments, then removing points that don't significantly contribute to the shape using the Ramer-Douglas-Peucker algorithm. #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] -async fn decimate( +fn decimate( _: impl Ctx, /// The vector paths to decimate. #[implementations(Graphic, Vector)] @@ -2064,7 +2064,7 @@ async fn decimate( /// /// If multiple subpaths make up the path, the whole number part of the progression value selects the subpath and the decimal part determines the position along it. #[node_macro::node(category("Vector: Modifier"), path(graphene_core::vector))] -async fn cut_path( +fn cut_path( _: impl Ctx, /// The path to insert a cut into. #[implementations(Graphic, Vector)] @@ -2115,7 +2115,7 @@ async fn cut_path( /// Cuts path segments into separate disconnected pieces where each is a distinct subpath. #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] -async fn cut_segments(_: impl Ctx, #[implementations(Graphic, Vector)] content: Item) -> Item { +fn cut_segments(_: impl Ctx, #[implementations(Graphic, Vector)] content: Item) -> Item { V::map_vector_items(content, |content| { let mut content = content; // Iterate through every segment and make a copy of each of its endpoints, then reassign each segment's endpoints to its own unique point copy @@ -2177,7 +2177,7 @@ async fn cut_segments(_: impl Ctx, #[implementati /// /// If multiple subpaths make up the path, the whole number part of the progression value selects the subpath and the decimal part determines the position along it. #[node_macro::node(name("Position on Path"), category("Vector: Measure"), path(graphene_core::vector))] -async fn position_on_path( +fn position_on_path( _: impl Ctx, /// The path to traverse. content: Item, @@ -2215,7 +2215,7 @@ async fn position_on_path( /// /// If multiple subpaths make up the path, the whole number part of the progression value selects the subpath and the decimal part determines the position along it. #[node_macro::node(name("Tangent on Path"), category("Vector: Measure"), path(graphene_core::vector))] -async fn tangent_on_path( +fn tangent_on_path( _: impl Ctx, /// The path to traverse. content: Item, @@ -2257,7 +2257,7 @@ async fn tangent_on_path( } #[node_macro::node(category("Vector: Modifier"), path(core_types::vector), memoize)] -async fn scatter_points( +fn scatter_points( _: impl Ctx, #[implementations(Graphic, Vector)] content: Item, #[unit(" px")] @@ -2303,7 +2303,7 @@ async fn scatter_points( } #[node_macro::node(name("Spline"), category("Vector: Modifier"), path(core_types::vector))] -async fn spline(_: impl Ctx, #[implementations(Graphic, Vector)] content: Item) -> Item { +fn spline(_: impl Ctx, #[implementations(Graphic, Vector)] content: Item) -> Item { V::map_vector_items(content, |content| { let mut content = content; // Exit early if there are no points to generate splines from. @@ -2399,7 +2399,7 @@ fn apply_point_deltas(element: &mut Vector, deltas: &[DVec2], transform: DAffine /// Perturbs the positions of anchor points in vector geometry by random amounts and directions. #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] -async fn jitter_points( +fn jitter_points( _: impl Ctx, /// The vector geometry with points to be jittered. #[implementations(Graphic, Vector)] @@ -2455,7 +2455,7 @@ async fn jitter_points( /// Displaces anchor points along their normal direction (perpendicular to the path) by a set distance. /// Points with 0 or 3+ segment connections have no well-defined normal and are left in place. #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] -async fn offset_points( +fn offset_points( _: impl Ctx, /// The vector geometry with points to be offset. #[implementations(Graphic, Vector)] @@ -2497,7 +2497,7 @@ async fn offset_points( /// /// *Progression* morphs through all objects. Interpolation is linear unless *Path* geometry is provided to control the trajectory between key objects. The **Origins to Polyline** node may be used to create a path with anchor points corresponding to each object. Other nodes can modify its path segments. #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] -async fn morph( +fn morph( _: impl Ctx, /// The vector objects to interpolate between. Mixed graphic content is deeply flattened to keep only vector elements. content: List, @@ -3476,12 +3476,12 @@ fn point_inside(_: impl Ctx, source: Item, point: Item) -> Item Item { +fn list_length(_: impl Ctx, content: ListDyn) -> Item { Item::new_from_element(content.len() as f64) } #[node_macro::node(category("Vector: Measure"), path(graphene_core::vector))] -async fn count_points(_: impl Ctx, content: Item) -> Item { +fn count_points(_: impl Ctx, content: Item) -> Item { let count = content.element().point_domain.positions().len() as f64; Item::new_from_element(count) @@ -3490,7 +3490,7 @@ async fn count_points(_: impl Ctx, content: Item) -> Item { /// Retrieves the vec2 position (in local space) of the anchor point at the specified index within a vector element. /// If no value exists at that index, the position (0, 0) is returned. #[node_macro::node(category("Vector: Measure"), path(graphene_core::vector))] -async fn index_points( +fn index_points( _: impl Ctx, /// The vector element containing the anchor points to be retrieved. content: Item, @@ -3515,7 +3515,7 @@ async fn index_points( } #[node_macro::node(category("Vector: Measure"), path(core_types::vector))] -async fn path_length(_: impl Ctx, source: Item) -> Item { +fn path_length(_: impl Ctx, source: Item) -> Item { let transform: DAffine2 = source.attribute_cloned_or_default(ATTR_TRANSFORM); let length = source .element() @@ -3644,8 +3644,8 @@ mod test { async fn offset_path_does_not_duplicate_closing_anchors() { // Offsetting closed triangles must not leave each subpath with a redundant start/end anchor (a Kurbo offset // contour returns to approximately, not exactly, its start; that near-coincident point must close, not duplicate). - let delaunay = super::triangulate((), vector_item_from_points(&SQUARE_WITH_CENTER), item(false)).await; - let offset = super::offset_path((), delaunay, item(0.5), item(StrokeJoin::Miter), item(4.)).await; + let delaunay = super::triangulate((), vector_item_from_points(&SQUARE_WITH_CENTER), item(false)); + let offset = super::offset_path((), delaunay, item(0.5), item(StrokeJoin::Miter), item(4.)); let result = offset.element(); let mut subpaths = 0; @@ -3661,7 +3661,7 @@ mod test { #[tokio::test] async fn delaunay_disconnected_cells_make_one_subpath_per_triangle() { - let result = super::triangulate((), vector_item_from_points(&SQUARE_WITH_CENTER), item(false)).await; + let result = super::triangulate((), vector_item_from_points(&SQUARE_WITH_CENTER), item(false)); let vector = result.element(); // The square plus its center tessellates into four triangles, each its own closed subpath. assert_eq!(vector.stroke_manipulator_groups().filter(|(_, closed)| *closed).count(), 4); @@ -3691,8 +3691,8 @@ mod test { assert_eq!(subpath_winding_signs(&ellipse)[0], expected, "Rectangle and Ellipse should agree on winding"); // Delaunay and Voronoi must emit subpaths that wind the same way as those generators. - let delaunay = super::triangulate((), vector_item_from_points(&SQUARE_WITH_CENTER), item(false)).await; - let voronoi = super::voronoi_cells((), vector_item_from_points(&SQUARE_WITH_CENTER), item(false)).await; + let delaunay = super::triangulate((), vector_item_from_points(&SQUARE_WITH_CENTER), item(false)); + let voronoi = super::voronoi_cells((), vector_item_from_points(&SQUARE_WITH_CENTER), item(false)); for sign in subpath_winding_signs(delaunay.element()) { assert_eq!(sign, expected, "Delaunay subpath winding should match the Rectangle/Ellipse generators"); } @@ -3703,7 +3703,7 @@ mod test { #[tokio::test] async fn delaunay_shared_mesh_welds_points_and_shares_edges() { - let result = super::triangulate((), vector_item_from_points(&SQUARE_WITH_CENTER), item(true)).await; + let result = super::triangulate((), vector_item_from_points(&SQUARE_WITH_CENTER), item(true)); let vector = result.element(); // The connected mesh reuses the five input points and shares edges, so it fills face by face. assert!(vector.use_face_fill()); @@ -3714,7 +3714,7 @@ mod test { #[tokio::test] async fn voronoi_disconnected_cells_make_a_subpath_per_cell() { - let result = super::voronoi_cells((), vector_item_from_points(&SQUARE_WITH_CENTER), item(false)).await; + let result = super::voronoi_cells((), vector_item_from_points(&SQUARE_WITH_CENTER), item(false)); let vector = result.element(); let cells = vector.stroke_manipulator_groups().filter(|(_, closed)| *closed).count(); assert!(cells > 0, "expected at least one Voronoi cell"); @@ -3730,7 +3730,7 @@ mod test { #[tokio::test] async fn voronoi_shared_mesh_uses_face_fill() { - let result = super::voronoi_cells((), vector_item_from_points(&SQUARE_WITH_CENTER), item(true)).await; + let result = super::voronoi_cells((), vector_item_from_points(&SQUARE_WITH_CENTER), item(true)); let vector = result.element(); assert!(vector.use_face_fill()); assert!(!vector.segment_domain.ids().is_empty()); @@ -3740,7 +3740,7 @@ mod test { async fn voronoi_leaves_degenerate_input_untouched() { // Two points cannot form a diagram, so the element passes through unchanged. let points = [DVec2::new(0., 0.), DVec2::new(1., 1.)]; - let result = super::voronoi_cells((), vector_item_from_points(&points), item(false)).await; + let result = super::voronoi_cells((), vector_item_from_points(&points), item(false)); let vector = result.element(); assert_eq!(vector.point_domain.ids().len(), 2); assert_eq!(vector.segment_domain.ids().len(), 0); @@ -3757,7 +3757,7 @@ mod test { DVec2::new(3., 4.), DVec2::new(7., 5.), ]; - let result = super::relax_points((), vector_item_from_points(&points), item(2.)).await; + let result = super::relax_points((), vector_item_from_points(&points), item(2.)); let vector = result.element(); // Relaxation preserves the point count but repositions the interior anchors within the hull. @@ -3777,7 +3777,7 @@ mod test { #[tokio::test] async fn bounding_box() { - let bounding_box = super::bounding_box((), Item::new_from_element(Vector::from_bezpath(Rect::new(-1., -1., 1., 1.).to_path(DEFAULT_ACCURACY)))).await; + let bounding_box = super::bounding_box((), Item::new_from_element(Vector::from_bezpath(Rect::new(-1., -1., 1., 1.).to_path(DEFAULT_ACCURACY)))); let bounding_box = bounding_box.element(); assert_eq!(bounding_box.stroke_manipulator_groups().count(), 1); let manipulator_groups_anchors = bounding_box @@ -3829,9 +3829,8 @@ mod test { Item::new_from_element(0), Item::new_from_element(0.), Item::new_from_element(0), - ) - .await; - let combined = List::new_from_item(super::combine_paths(Footprint::default(), List::new_from_element(Graphic::VectorList(copy_to_points))).await); + ); + let combined = List::new_from_item(super::combine_paths(Footprint::default(), List::new_from_element(Graphic::VectorList(copy_to_points)))); let combined_copy_to_points = combined.element(0).unwrap(); assert_eq!(combined_copy_to_points.stroke_manipulator_groups().count(), expected_points.len()); @@ -3858,8 +3857,7 @@ mod test { Item::new_from_element(0.), Item::new_from_element(0.), Item::new_from_element(false), - ) - .await; + ); let sample_polyline = sample_polyline.element(); assert_eq!(sample_polyline.point_domain.positions().len(), 4); for (pos, expected) in sample_polyline.point_domain.positions().iter().zip([DVec2::X * 0., DVec2::X * 30., DVec2::X * 60., DVec2::X * 90.]) { @@ -3878,8 +3876,7 @@ mod test { Item::new_from_element(45.), Item::new_from_element(10.), Item::new_from_element(true), - ) - .await; + ); let sample_polyline = sample_polyline.element(); assert_eq!(sample_polyline.point_domain.positions().len(), 4); for (pos, expected) in sample_polyline.point_domain.positions().iter().zip([DVec2::X * 45., DVec2::X * 60., DVec2::X * 75., DVec2::X * 90.]) { @@ -3893,8 +3890,7 @@ mod test { vector_item_from_bezpath(ellipse_bezpath(DVec2::splat(-50.), DVec2::splat(50.))), Item::new_from_element(10. * std::f64::consts::SQRT_2), Item::new_from_element(0), - ) - .await; + ); let poisson_points = poisson_points.element(); assert!( (20..=40).contains(&poisson_points.point_domain.positions().len()), @@ -3911,14 +3907,14 @@ mod test { let transform = DAffine2::from_scale(DVec2::new(2., 2.)); let item = create_vector_item(bezpath, transform); - let length = super::path_length(Footprint::default(), item).await; + let length = super::path_length(Footprint::default(), item); // 101 (each rectangle edge length) * 4 (rectangle perimeter) * 2 (scale) assert_eq!(length.into_element(), 101. * 4. * 2.); } #[tokio::test] async fn spline() { - let spline = super::spline(Footprint::default(), vector_item_from_bezpath(Rect::new(0., 0., 100., 100.).to_path(DEFAULT_ACCURACY))).await; + let spline = super::spline(Footprint::default(), vector_item_from_bezpath(Rect::new(0., 0., 100., 100.).to_path(DEFAULT_ACCURACY))); let spline = spline.element(); assert_eq!(spline.stroke_bezpath_iter().count(), 1); assert_eq!(spline.point_domain.positions(), &[DVec2::ZERO, DVec2::new(100., 0.), DVec2::new(100., 100.), DVec2::new(0., 100.)]); @@ -3937,8 +3933,7 @@ mod test { Item::new_from_element(false), Item::new_from_element(InterpolationDistribution::default()), Item::default(), - ) - .await; + ); let morphed = List::new_from_item(morphed); let morphed_element = morphed.element(0).unwrap(); // Geometry stays in local space (original rectangle coordinates) @@ -3976,8 +3971,7 @@ mod test { Item::new_from_element(false), Item::new_from_element(InterpolationDistribution::default()), Item::default(), - ) - .await; + ); let morphed = List::new_from_item(morphed); let appearance = morphed.attribute::(ATTR_APPEARANCE, 0).expect("Morph should keep the appearance at the midpoint"); @@ -4032,8 +4026,7 @@ mod test { Item::new_from_element(false), Item::new_from_element(InterpolationDistribution::default()), Item::default(), - ) - .await; + ); let morphed = List::new_from_item(morphed); let appearance = morphed.attribute::(ATTR_APPEARANCE, 0).expect("Morph should keep the appearance at the midpoint"); @@ -4192,8 +4185,7 @@ mod test { Item::new_from_element(false), Item::new_from_element(0), Item::new_from_element(0_i64), - ) - .await; + ); let Some(Graphic::Vector(item)) = styled.element(0) else { panic!("the leaf should stay a rank-0 vector") From 81f07988b156b40c9a66c8864c0a8c549930c35b Mon Sep 17 00:00:00 2001 From: Timon Date: Tue, 29 Sep 2026 13:07:13 +0000 Subject: [PATCH 04/54] Remove unneeded async from graphic nodes --- node-graph/nodes/graphic/src/graphic.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/node-graph/nodes/graphic/src/graphic.rs b/node-graph/nodes/graphic/src/graphic.rs index e66834ced0e..1d733377ccd 100644 --- a/node-graph/nodes/graphic/src/graphic.rs +++ b/node-graph/nodes/graphic/src/graphic.rs @@ -432,7 +432,7 @@ async fn map( } #[node_macro::node(category("General"))] -async fn mirror( +fn mirror( _: impl Ctx, #[implementations( Graphic, @@ -813,7 +813,7 @@ fn read_attribute_raster( /// Joins two lists of the same type, extending the base list with the items from the new list. #[node_macro::node(category("General"))] -pub async fn extend( +pub fn extend( _: impl Ctx, /// The list whose items will appear at the start of the extended list. #[implementations( @@ -863,7 +863,7 @@ pub async fn extend( /// Performs an obsolete function as part of a migration from an older document format. /// Users are advised to delete this node and replace it with a new one. #[node_macro::node(category(""))] -pub async fn legacy_layer_extend( +pub fn legacy_layer_extend( _: impl Ctx, #[implementations(List, List, List, List, List>, List>, List, List)] base: List, #[expose] @@ -890,7 +890,7 @@ pub async fn legacy_layer_extend( /// Nests the input graphical content in a wrapper graphic, collecting it all into a single group. /// The inverse of this node is 'Flatten Graphic'. #[node_macro::node(category("General"))] -pub async fn into_group + 'n>( +pub fn into_group + 'n>( _: impl Ctx, #[implementations( List, @@ -912,13 +912,13 @@ pub async fn into_group + 'n>( /// Type-asserts a value to be graphical content, converting each item of other content types into its matching form. /// Use the 'Into Group' node instead to collect the content into a single group. #[node_macro::node(category("General"))] -pub async fn as_graphic(_: impl Ctx, value: Item) -> Item { +pub fn as_graphic(_: impl Ctx, value: Item) -> Item { value } /// Removes a level of nesting from a `Graphic[]`, or all nesting if "Fully Flatten" is enabled. #[node_macro::node(category("General"))] -pub async fn flatten_graphic(_: impl Ctx, content: List, fully_flatten: Item) -> List { +pub fn flatten_graphic(_: impl Ctx, content: List, fully_flatten: Item) -> List { let fully_flatten = fully_flatten.into_element(); // TODO: Avoid mutable reference, instead return a new List? @@ -963,7 +963,7 @@ pub async fn flatten_graphic(_: impl Ctx, content: List, fully_flatten: /// Converts a `Graphic[]` into a `Vector[]` by deeply flattening any vector content it contains, and discarding any non-vector content. #[node_macro::node(category("Vector"))] -pub async fn flatten_vector(_: impl Ctx, content: List) -> List { +pub fn flatten_vector(_: impl Ctx, content: List) -> List { let mut output: List = content.clone().into_flattened_list(); // TODO: Replace this snapshot hack with per-layer metadata driven by each layer's Monitor node. @@ -995,19 +995,19 @@ pub async fn flatten_vector(_: impl Ctx, content: List) -> List /// Converts a `Graphic[]` into a `Raster[]` by deeply flattening any raster content it contains, and discarding any non-raster content. #[node_macro::node(category("Raster"))] -pub async fn flatten_raster(_: impl Ctx, content: List) -> List> { +pub fn flatten_raster(_: impl Ctx, content: List) -> List> { content.into_flattened_list() } /// Converts a `Graphic[]` into a `Color[]` by deeply flattening any color content it contains, and discarding any non-color content. #[node_macro::node(category("General"))] -pub async fn flatten_color(_: impl Ctx, content: List) -> List { +pub fn flatten_color(_: impl Ctx, content: List) -> List { content.into_flattened_list() } /// Converts a `Graphic[]` into a `Gradient[]` by deeply flattening any gradient content it contains, and discarding any non-gradient content. #[node_macro::node(category("General"))] -pub async fn flatten_gradient(_: impl Ctx, content: List) -> List { +pub fn flatten_gradient(_: impl Ctx, content: List) -> List { content.into_flattened_list() } From fe482b846177c2f528aa99aa1543338436d2f393 Mon Sep 17 00:00:00 2001 From: Timon Date: Tue, 29 Sep 2026 13:07:13 +0000 Subject: [PATCH 05/54] Remove unneeded async from raster nodes --- node-graph/nodes/raster/src/adjustments.rs | 6 +++--- node-graph/nodes/raster/src/dehaze.rs | 2 +- node-graph/nodes/raster/src/filter.rs | 4 ++-- node-graph/nodes/raster/src/image_color_palette.rs | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/node-graph/nodes/raster/src/adjustments.rs b/node-graph/nodes/raster/src/adjustments.rs index 0ba42d9baae..7b08b7d6b2a 100644 --- a/node-graph/nodes/raster/src/adjustments.rs +++ b/node-graph/nodes/raster/src/adjustments.rs @@ -653,7 +653,7 @@ fn levels>( // Needs the heap for its curves, so it stays off the shader build for now. #[cfg(feature = "std")] #[node_macro::node(category("Raster: Adjustment"), properties("transfer_curves_properties"))] -async fn curves + Send>( +fn curves + Send>( _: impl Ctx, #[implementations(Raster, Color, Gradient)] image: Item, curve: Item, @@ -1428,7 +1428,7 @@ fn threshold>( // TODO: - The ramp is `(1 - s) * linear + s * smooth` for smoothness s, clamped per interval to its two stop colors. #[cfg(feature = "std")] #[node_macro::node(category("Raster: Adjustment"))] -async fn gradient_map + Send>( +fn gradient_map + Send>( _: impl Ctx, #[implementations(Raster, Color, Gradient)] image: Item, #[default(Color::BLACK, Color::WHITE)] gradient: Item, @@ -2501,7 +2501,7 @@ fn luma_rec_601_fixed_point(r: f32, g: f32, b: f32) -> f32 { // TODO: Verify the tetrahedral interpolation, which is unconfirmed against other implementations. #[cfg(feature = "std")] #[node_macro::node(category("Raster: Adjustment"))] -async fn color_lookup + Send>( +fn color_lookup + Send>( _: impl Ctx, /// The image whose colors are remapped by the LUT (lookup table). #[implementations(Raster, Color, Gradient)] diff --git a/node-graph/nodes/raster/src/dehaze.rs b/node-graph/nodes/raster/src/dehaze.rs index d93e1776dfb..37390852d96 100644 --- a/node-graph/nodes/raster/src/dehaze.rs +++ b/node-graph/nodes/raster/src/dehaze.rs @@ -7,7 +7,7 @@ use raster_types::{CPU, Raster}; use std::cmp::{max, min}; #[node_macro::node(category("Raster: Filter"))] -async fn dehaze( +fn dehaze( _: impl Ctx, image_frame: Item>, #[unit("%")] diff --git a/node-graph/nodes/raster/src/filter.rs b/node-graph/nodes/raster/src/filter.rs index ac70828e770..40bfaf8deed 100644 --- a/node-graph/nodes/raster/src/filter.rs +++ b/node-graph/nodes/raster/src/filter.rs @@ -86,7 +86,7 @@ fn unpremultiply_gamma_to_linear(buffer: Image) -> Imag /// Blurs the image with a Gaussian or box blur kernel filter. #[node_macro::node(category("Raster: Filter"))] -async fn blur( +fn blur( _: impl Ctx, /// The image to be blurred. image_frame: Item>, @@ -121,7 +121,7 @@ async fn blur( /// Applies a median filter to reduce noise while preserving edges. #[node_macro::node(category("Raster: Filter"))] -async fn median_filter( +fn median_filter( _: impl Ctx, /// The image to be filtered. image_frame: Item>, diff --git a/node-graph/nodes/raster/src/image_color_palette.rs b/node-graph/nodes/raster/src/image_color_palette.rs index 06e6b0c395a..8723a16f0c5 100644 --- a/node-graph/nodes/raster/src/image_color_palette.rs +++ b/node-graph/nodes/raster/src/image_color_palette.rs @@ -4,7 +4,7 @@ use core_types::list::{Item, List}; use raster_types::{CPU, Raster}; #[node_macro::node(category("Color"))] -async fn image_color_palette( +fn image_color_palette( _: impl Ctx, image: Item>, #[default(4)] @@ -75,6 +75,6 @@ mod test { })), Item::new_from_element(1), ); - assert_eq!(futures::executor::block_on(result), List::new_from_element(Color::from_rgbaf32(0., 0., 0., 1.).unwrap())); + assert_eq!(result, List::new_from_element(Color::from_rgbaf32(0., 0., 0., 1.).unwrap())); } } From 0d43744be6a3fcef1347616862c15e6f04f42756 Mon Sep 17 00:00:00 2001 From: Timon Date: Tue, 29 Sep 2026 13:07:13 +0000 Subject: [PATCH 06/54] Remove unneeded async from the remaining nodes --- node-graph/nodes/brush/src/basic_brush/mod.rs | 2 +- node-graph/nodes/gcore/src/context.rs | 4 ++-- node-graph/nodes/gstd/src/platform_application_io.rs | 8 ++++---- node-graph/nodes/gstd/src/render_background.rs | 2 +- node-graph/nodes/gstd/src/render_pixel_preview.rs | 2 +- node-graph/nodes/path-bool/src/lib.rs | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/node-graph/nodes/brush/src/basic_brush/mod.rs b/node-graph/nodes/brush/src/basic_brush/mod.rs index 3fd304cef7c..9dedc3c7175 100644 --- a/node-graph/nodes/brush/src/basic_brush/mod.rs +++ b/node-graph/nodes/brush/src/basic_brush/mod.rs @@ -65,7 +65,7 @@ pub async fn basic_brush<'a: 'n>( } #[node_macro::node(category(""), inject_scope)] -async fn basic_brush_pipeline<'a: 'n>( +fn basic_brush_pipeline<'a: 'n>( _ctx: impl Ctx, #[scope(ProtoNodeIdentifier::new("graphene_std::platform_application_io::WgpuExecutorNode"))] executor: Item<&'a WgpuExecutor>, #[data] pipeline: WgpuPipelineCache, diff --git a/node-graph/nodes/gcore/src/context.rs b/node-graph/nodes/gcore/src/context.rs index 30dbf2d90f1..e2f7f30b837 100644 --- a/node-graph/nodes/gcore/src/context.rs +++ b/node-graph/nodes/gcore/src/context.rs @@ -66,7 +66,7 @@ fn read_number(ctx: impl Ctx + ExtractVarArgs) -> Item { } #[node_macro::node(category("Context"), path(core_types::vector))] -async fn read_position( +fn read_position( ctx: impl Ctx + ExtractPosition, _primary: (), /// The number of nested loops to traverse outwards (from the innermost loop) to get the position from. The most upstream loop is level 0, and downstream loops add levels. @@ -85,7 +85,7 @@ async fn read_position( /// /// Nested loops can enable 2D or higher-dimensional iteration by using the *Loop Level* parameter to read the index from outer levels of loops. #[node_macro::node(category("Context"), path(core_types::vector))] -async fn read_index( +fn read_index( ctx: impl Ctx + ExtractIndex, _primary: (), /// The number of nested loops to traverse outwards (from the innermost loop) to get the index from. The most upstream loop is level 0, and downstream loops add levels. diff --git a/node-graph/nodes/gstd/src/platform_application_io.rs b/node-graph/nodes/gstd/src/platform_application_io.rs index 2441e793eab..3c3f33f23fe 100644 --- a/node-graph/nodes/gstd/src/platform_application_io.rs +++ b/node-graph/nodes/gstd/src/platform_application_io.rs @@ -173,7 +173,7 @@ fn decode_image(_: impl Ctx, data: Item) -> Item> { #[cfg(target_family = "wasm")] #[node_macro::node(category(""))] -async fn create_canvas(_: impl Ctx) -> Item { +fn create_canvas(_: impl Ctx) -> Item { Item::new_from_element(CanvasHandle::new()) } @@ -239,7 +239,7 @@ async fn rasterize(_: impl Ctx, data: List, footprint: Item, } #[node_macro::node(category(""), inject_scope)] -pub async fn editor_api<'a: 'n>(_: impl Ctx, #[scope("editor-api")] editor_api: Item<&'a PlatformEditorApi>) -> Item<&'a PlatformEditorApi> { +pub fn editor_api<'a: 'n>(_: impl Ctx, #[scope("editor-api")] editor_api: Item<&'a PlatformEditorApi>) -> Item<&'a PlatformEditorApi> { editor_api } @@ -269,7 +269,7 @@ pub async fn resource<'a: 'n>( } #[node_macro::node(category(""), inject_scope)] -pub async fn wgpu_executor<'a: 'n>(_: impl Ctx, #[scope(editor_api::IDENTIFIER)] editor_api: Item<&'a PlatformEditorApi>) -> Item<&'a ::wgpu_executor::WgpuExecutor> { +pub fn wgpu_executor<'a: 'n>(_: impl Ctx, #[scope(editor_api::IDENTIFIER)] editor_api: Item<&'a PlatformEditorApi>) -> Item<&'a ::wgpu_executor::WgpuExecutor> { let executor = editor_api .into_element() .application_io @@ -281,7 +281,7 @@ pub async fn wgpu_executor<'a: 'n>(_: impl Ctx, #[scope(editor_api::IDENTIFIER)] } #[node_macro::node(category(""), inject_scope)] -pub async fn try_wgpu_executor<'a: 'n>(_: impl Ctx, #[scope(editor_api::IDENTIFIER)] editor_api: Item<&'a PlatformEditorApi>) -> Item> { +pub fn try_wgpu_executor<'a: 'n>(_: impl Ctx, #[scope(editor_api::IDENTIFIER)] editor_api: Item<&'a PlatformEditorApi>) -> Item> { let executor = editor_api.into_element().application_io.as_ref().and_then(|application_io| application_io.gpu_executor()); Item::new_from_element(executor) } diff --git a/node-graph/nodes/gstd/src/render_background.rs b/node-graph/nodes/gstd/src/render_background.rs index 45abc4429e4..3de95a83e9f 100644 --- a/node-graph/nodes/gstd/src/render_background.rs +++ b/node-graph/nodes/gstd/src/render_background.rs @@ -122,7 +122,7 @@ async fn render_background<'a: 'n>( } #[node_macro::node(category(""), inject_scope)] -async fn composite_background_pipeline<'a: 'n>( +fn composite_background_pipeline<'a: 'n>( _ctx: impl Ctx, #[scope(crate::platform_application_io::try_wgpu_executor::IDENTIFIER)] executor: Item>, #[data] pipeline: WgpuPipelineCache, diff --git a/node-graph/nodes/gstd/src/render_pixel_preview.rs b/node-graph/nodes/gstd/src/render_pixel_preview.rs index eb1546b0227..7e86509b160 100644 --- a/node-graph/nodes/gstd/src/render_pixel_preview.rs +++ b/node-graph/nodes/gstd/src/render_pixel_preview.rs @@ -79,7 +79,7 @@ pub async fn render_pixel_preview<'a: 'n>( } #[node_macro::node(category(""), inject_scope)] -async fn pixel_preview_pipeline<'a: 'n>( +fn pixel_preview_pipeline<'a: 'n>( _ctx: impl Ctx, #[scope(crate::platform_application_io::try_wgpu_executor::IDENTIFIER)] executor: Item>, #[data] pipeline: WgpuPipelineCache, diff --git a/node-graph/nodes/path-bool/src/lib.rs b/node-graph/nodes/path-bool/src/lib.rs index fdef8370448..82ae5b2d66d 100644 --- a/node-graph/nodes/path-bool/src/lib.rs +++ b/node-graph/nodes/path-bool/src/lib.rs @@ -18,7 +18,7 @@ pub use vector_types::vector::misc::BooleanOperation; /// Combines the geometric forms of one or more closed paths into a new vector path that results from cutting or joining the paths by the chosen method. #[node_macro::node(category("Vector: Modifier"), memoize)] -async fn boolean_operation( +fn boolean_operation( _: impl Ctx, /// The `List` of vector paths to perform the boolean operation on. Nested `List`s are automatically flattened. content: List, From 206aee1246da332a62d28f5c2b668e573a300dcf Mon Sep 17 00:00:00 2001 From: Timon Date: Tue, 29 Sep 2026 13:50:03 +0000 Subject: [PATCH 07/54] Make most node fns private --- node-graph/nodes/brush/src/basic_brush/mod.rs | 2 +- node-graph/nodes/graphic/src/artboard.rs | 2 +- node-graph/nodes/graphic/src/graphic.rs | 22 +++++++++---------- .../nodes/gstd/src/platform_application_io.rs | 10 ++++----- node-graph/nodes/gstd/src/render_cache.rs | 2 +- .../nodes/gstd/src/render_pixel_preview.rs | 2 +- node-graph/nodes/raster/src/std_nodes.rs | 16 +++++++------- node-graph/nodes/repeat/src/repeat_nodes.rs | 2 +- 8 files changed, 29 insertions(+), 29 deletions(-) diff --git a/node-graph/nodes/brush/src/basic_brush/mod.rs b/node-graph/nodes/brush/src/basic_brush/mod.rs index 9dedc3c7175..a8520b548f2 100644 --- a/node-graph/nodes/brush/src/basic_brush/mod.rs +++ b/node-graph/nodes/brush/src/basic_brush/mod.rs @@ -15,7 +15,7 @@ use raster_types::{GPU, Raster}; use wgpu_executor::{WgpuExecutor, WgpuPipelineCache}; #[node_macro::node(category("Raster: Brush"))] -pub async fn basic_brush<'a: 'n>( +async fn basic_brush<'a: 'n>( ctx: impl Ctx + ExtractFootprint, strokes: List, #[widget(ParsedWidgetOverride::Hidden)] cache: Item, diff --git a/node-graph/nodes/graphic/src/artboard.rs b/node-graph/nodes/graphic/src/artboard.rs index 4f20a023c3d..eab78e67465 100644 --- a/node-graph/nodes/graphic/src/artboard.rs +++ b/node-graph/nodes/graphic/src/artboard.rs @@ -9,7 +9,7 @@ use vector_types::Gradient; /// Constructs a single-element `Artboard[]` with the given content and metadata stored as row attributes. #[node_macro::node(category(""))] -pub async fn create_artboard( +async fn create_artboard( ctx: impl ExtractAll + CloneVarArgs + Ctx, /// Graphics to include within the artboard. #[implementations( diff --git a/node-graph/nodes/graphic/src/graphic.rs b/node-graph/nodes/graphic/src/graphic.rs index 1d733377ccd..9f5f338a0f0 100644 --- a/node-graph/nodes/graphic/src/graphic.rs +++ b/node-graph/nodes/graphic/src/graphic.rs @@ -15,7 +15,7 @@ use vector_types::{Gradient, ReferencePoint}; /// Returns the list with the item at the specified index removed. /// If no value exists at that index, the list is returned unchanged. #[node_macro::node(category("General"), name("Remove at Index"))] -pub fn remove_at_index( +fn remove_at_index( _: impl Ctx, /// The list of data. #[implementations( @@ -49,7 +49,7 @@ pub fn remove_at_index( /// Returns the item at the specified index in a list, keeping its attributes. /// If no value exists at that index, the element type's default is returned. #[node_macro::node(category("General"), name("Item at Index"))] -pub fn item_at_index( +fn item_at_index( _: impl Ctx, /// The list of data to take the item from. #[implementations( @@ -503,7 +503,7 @@ fn mirror( /// editor tools (e.g. selection, click target routing) trace data back to its owning layer regardless of whether /// the layer is at the root document network or nested inside a custom subgraph. #[node_macro::node(name("Path of Subgraph"), category(""))] -pub fn path_of_subgraph(_: impl Ctx, node_path: Item) -> Item { +fn path_of_subgraph(_: impl Ctx, node_path: Item) -> Item { let node_path = node_path.into_element().0; let len = node_path.len(); Item::new_from_element(NodeIdPath(node_path.into_iter().take(len.saturating_sub(1)).collect())) @@ -813,7 +813,7 @@ fn read_attribute_raster( /// Joins two lists of the same type, extending the base list with the items from the new list. #[node_macro::node(category("General"))] -pub fn extend( +fn extend( _: impl Ctx, /// The list whose items will appear at the start of the extended list. #[implementations( @@ -863,7 +863,7 @@ pub fn extend( /// Performs an obsolete function as part of a migration from an older document format. /// Users are advised to delete this node and replace it with a new one. #[node_macro::node(category(""))] -pub fn legacy_layer_extend( +fn legacy_layer_extend( _: impl Ctx, #[implementations(List, List, List, List, List>, List>, List, List)] base: List, #[expose] @@ -890,7 +890,7 @@ pub fn legacy_layer_extend( /// Nests the input graphical content in a wrapper graphic, collecting it all into a single group. /// The inverse of this node is 'Flatten Graphic'. #[node_macro::node(category("General"))] -pub fn into_group + 'n>( +fn into_group + 'n>( _: impl Ctx, #[implementations( List, @@ -912,13 +912,13 @@ pub fn into_group + 'n>( /// Type-asserts a value to be graphical content, converting each item of other content types into its matching form. /// Use the 'Into Group' node instead to collect the content into a single group. #[node_macro::node(category("General"))] -pub fn as_graphic(_: impl Ctx, value: Item) -> Item { +fn as_graphic(_: impl Ctx, value: Item) -> Item { value } /// Removes a level of nesting from a `Graphic[]`, or all nesting if "Fully Flatten" is enabled. #[node_macro::node(category("General"))] -pub fn flatten_graphic(_: impl Ctx, content: List, fully_flatten: Item) -> List { +fn flatten_graphic(_: impl Ctx, content: List, fully_flatten: Item) -> List { let fully_flatten = fully_flatten.into_element(); // TODO: Avoid mutable reference, instead return a new List? @@ -995,19 +995,19 @@ pub fn flatten_vector(_: impl Ctx, content: List) -> List { /// Converts a `Graphic[]` into a `Raster[]` by deeply flattening any raster content it contains, and discarding any non-raster content. #[node_macro::node(category("Raster"))] -pub fn flatten_raster(_: impl Ctx, content: List) -> List> { +fn flatten_raster(_: impl Ctx, content: List) -> List> { content.into_flattened_list() } /// Converts a `Graphic[]` into a `Color[]` by deeply flattening any color content it contains, and discarding any non-color content. #[node_macro::node(category("General"))] -pub fn flatten_color(_: impl Ctx, content: List) -> List { +fn flatten_color(_: impl Ctx, content: List) -> List { content.into_flattened_list() } /// Converts a `Graphic[]` into a `Gradient[]` by deeply flattening any gradient content it contains, and discarding any non-gradient content. #[node_macro::node(category("General"))] -pub fn flatten_gradient(_: impl Ctx, content: List) -> List { +fn flatten_gradient(_: impl Ctx, content: List) -> List { content.into_flattened_list() } diff --git a/node-graph/nodes/gstd/src/platform_application_io.rs b/node-graph/nodes/gstd/src/platform_application_io.rs index 3c3f33f23fe..b0447eef635 100644 --- a/node-graph/nodes/gstd/src/platform_application_io.rs +++ b/node-graph/nodes/gstd/src/platform_application_io.rs @@ -239,12 +239,12 @@ async fn rasterize(_: impl Ctx, data: List, footprint: Item, } #[node_macro::node(category(""), inject_scope)] -pub fn editor_api<'a: 'n>(_: impl Ctx, #[scope("editor-api")] editor_api: Item<&'a PlatformEditorApi>) -> Item<&'a PlatformEditorApi> { +fn editor_api<'a: 'n>(_: impl Ctx, #[scope("editor-api")] editor_api: Item<&'a PlatformEditorApi>) -> Item<&'a PlatformEditorApi> { editor_api } #[node_macro::node(category(""))] -pub async fn resource<'a: 'n>( +async fn resource<'a: 'n>( _: impl Ctx, /// The scope-provided editor API giving access to the platform's resource storage. #[scope(editor_api::IDENTIFIER)] @@ -269,7 +269,7 @@ pub async fn resource<'a: 'n>( } #[node_macro::node(category(""), inject_scope)] -pub fn wgpu_executor<'a: 'n>(_: impl Ctx, #[scope(editor_api::IDENTIFIER)] editor_api: Item<&'a PlatformEditorApi>) -> Item<&'a ::wgpu_executor::WgpuExecutor> { +fn wgpu_executor<'a: 'n>(_: impl Ctx, #[scope(editor_api::IDENTIFIER)] editor_api: Item<&'a PlatformEditorApi>) -> Item<&'a ::wgpu_executor::WgpuExecutor> { let executor = editor_api .into_element() .application_io @@ -281,14 +281,14 @@ pub fn wgpu_executor<'a: 'n>(_: impl Ctx, #[scope(editor_api::IDENTIFIER)] edito } #[node_macro::node(category(""), inject_scope)] -pub fn try_wgpu_executor<'a: 'n>(_: impl Ctx, #[scope(editor_api::IDENTIFIER)] editor_api: Item<&'a PlatformEditorApi>) -> Item> { +fn try_wgpu_executor<'a: 'n>(_: impl Ctx, #[scope(editor_api::IDENTIFIER)] editor_api: Item<&'a PlatformEditorApi>) -> Item> { let executor = editor_api.into_element().application_io.as_ref().and_then(|application_io| application_io.gpu_executor()); Item::new_from_element(executor) } /// Uploads image data from CPU memory into a GPU texture so that GPU-based nodes can process it. #[node_macro::node(category("Debug"), memoize)] -pub async fn upload_texture<'a: 'n>(_: impl Ctx, content: Item>, #[scope(wgpu_executor::IDENTIFIER)] executor: Item<&'a ::wgpu_executor::WgpuExecutor>) -> Item> { +async fn upload_texture<'a: 'n>(_: impl Ctx, content: Item>, #[scope(wgpu_executor::IDENTIFIER)] executor: Item<&'a ::wgpu_executor::WgpuExecutor>) -> Item> { let executor = executor.into_element(); let (raster, attributes) = content.into_parts(); diff --git a/node-graph/nodes/gstd/src/render_cache.rs b/node-graph/nodes/gstd/src/render_cache.rs index e2a92f0ff2d..683bac50ad3 100644 --- a/node-graph/nodes/gstd/src/render_cache.rs +++ b/node-graph/nodes/gstd/src/render_cache.rs @@ -322,7 +322,7 @@ fn flood_fill(start: &TileCoord, tile_set: &HashSet, visited: &mut Ha } #[node_macro::node(category(""))] -pub async fn render_output_cache<'a: 'n>( +async fn render_output_cache<'a: 'n>( ctx: impl Ctx + ExtractAll + CloneVarArgs + ExtractRealTime + ExtractAnimationTime + ExtractPointerPosition + Sync, #[scope(crate::platform_application_io::try_wgpu_executor::IDENTIFIER)] executor: Item>, #[scope(crate::platform_application_io::editor_api::IDENTIFIER)] editor_api: Item<&'a PlatformEditorApi>, diff --git a/node-graph/nodes/gstd/src/render_pixel_preview.rs b/node-graph/nodes/gstd/src/render_pixel_preview.rs index 7e86509b160..427e1a42143 100644 --- a/node-graph/nodes/gstd/src/render_pixel_preview.rs +++ b/node-graph/nodes/gstd/src/render_pixel_preview.rs @@ -9,7 +9,7 @@ use vector_types::vector::style::RenderMode; use wgpu_executor::{AsyncWgpuPipeline, WgpuExecutor, WgpuPipelineCache}; #[node_macro::node(category(""))] -pub async fn render_pixel_preview<'a: 'n>( +async fn render_pixel_preview<'a: 'n>( ctx: impl Ctx + ExtractAll + CloneVarArgs + Sync, #[scope(pixel_preview_pipeline::IDENTIFIER)] pipeline: Item, data: impl Node, Output = Item> + Send + Sync, diff --git a/node-graph/nodes/raster/src/std_nodes.rs b/node-graph/nodes/raster/src/std_nodes.rs index fb1dfa2dd20..9cb7168cb13 100644 --- a/node-graph/nodes/raster/src/std_nodes.rs +++ b/node-graph/nodes/raster/src/std_nodes.rs @@ -31,7 +31,7 @@ impl From for Error { } #[node_macro::node(category("Debug"))] -pub fn sample_image(ctx: impl ExtractFootprint + Clone + Send, image_frame: Item>) -> Item> { +fn sample_image(ctx: impl ExtractFootprint + Clone + Send, image_frame: Item>) -> Item> { let image_frame_transform: DAffine2 = image_frame.attribute_cloned_or_default(ATTR_TRANSFORM); let footprint = ctx.footprint(); @@ -95,7 +95,7 @@ pub fn sample_image(ctx: impl ExtractFootprint + Clone + Send, image_frame: Item } #[node_macro::node(category("Raster: Channels"))] -pub fn combine_channels( +fn combine_channels( _: impl Ctx, _primary: (), #[expose] red: Item>, @@ -150,7 +150,7 @@ pub fn combine_channels( } #[node_macro::node(category("Raster"))] -pub fn mask( +fn mask( _: impl Ctx, /// The image to be masked. image: Item>, @@ -196,7 +196,7 @@ pub fn mask( } #[node_macro::node(category(""))] -pub fn extend_image_to_bounds(_: impl Ctx, image: Item>, bounds: Item) -> Item> { +fn extend_image_to_bounds(_: impl Ctx, image: Item>, bounds: Item) -> Item> { let bounds = *bounds.element(); let image_transform: DAffine2 = image.attribute_cloned_or_default(ATTR_TRANSFORM); @@ -240,7 +240,7 @@ pub fn extend_image_to_bounds(_: impl Ctx, image: Item>, bounds: Ite } #[node_macro::node(category("Debug"))] -pub fn empty_image(_: impl Ctx, transform: Item, color: Item) -> Item> { +fn empty_image(_: impl Ctx, transform: Item, color: Item) -> Item> { let transform = transform.into_element(); let width = transform.transform_vector2(DVec2::new(1., 0.)).length() as u32; let height = transform.transform_vector2(DVec2::new(0., 1.)).length() as u32; @@ -254,7 +254,7 @@ pub fn empty_image(_: impl Ctx, transform: Item, color: Item) - /// /// Reads PNG, JPG, GIF, WEBP, TIFF, BMP, TGA, ICO, HDR, and EXR files. Light brighter than white, as HDR and EXR files can hold, is clipped to white. #[node_macro::node(category("Raster"))] -pub fn image<'a: 'n>( +fn image<'a: 'n>( _: impl Ctx, _primary: (), /// The image file to display. @@ -270,7 +270,7 @@ pub fn image<'a: 'n>( /// Generates customizable procedural noise patterns. #[node_macro::node(category("Raster: Pattern"))] #[allow(clippy::too_many_arguments)] -pub fn noise_pattern( +fn noise_pattern( ctx: impl ExtractFootprint + Ctx, _primary: (), #[default(true)] clip: Item, @@ -435,7 +435,7 @@ pub fn noise_pattern( } #[node_macro::node(category("Raster: Pattern"))] -pub fn mandelbrot(ctx: impl ExtractFootprint + Send) -> Item> { +fn mandelbrot(ctx: impl ExtractFootprint + Send) -> Item> { let footprint = ctx.footprint(); let viewport_bounds = footprint.viewport_bounds_in_local_space(); diff --git a/node-graph/nodes/repeat/src/repeat_nodes.rs b/node-graph/nodes/repeat/src/repeat_nodes.rs index 93d07ae40ee..1637ba3c0b1 100644 --- a/node-graph/nodes/repeat/src/repeat_nodes.rs +++ b/node-graph/nodes/repeat/src/repeat_nodes.rs @@ -53,7 +53,7 @@ async fn repeat( } #[node_macro::node(category("Repeat"))] -pub async fn repeat_array( +async fn repeat_array( ctx: impl ExtractAll + CloneVarArgs + Ctx, #[implementations( Context -> List, From 26be1d39705094fe8bd1acfd4a6746a4d238d99b Mon Sep 17 00:00:00 2001 From: Timon Date: Tue, 29 Sep 2026 14:13:46 +0000 Subject: [PATCH 08/54] Make node construction sync --- .../src/dynamic_executor.rs | 30 ++++++++----------- .../libraries/core-types/src/registry.rs | 2 +- node-graph/node-macro/src/codegen.rs | 12 +++----- 3 files changed, 18 insertions(+), 26 deletions(-) diff --git a/node-graph/interpreted-executor/src/dynamic_executor.rs b/node-graph/interpreted-executor/src/dynamic_executor.rs index ffa2c7ddb50..eda5a871a51 100644 --- a/node-graph/interpreted-executor/src/dynamic_executor.rs +++ b/node-graph/interpreted-executor/src/dynamic_executor.rs @@ -48,11 +48,11 @@ pub struct ResolvedDocumentNodeTypesDelta { } impl DynamicExecutor { - pub async fn new(proto_network: ProtoNetwork) -> Result { + pub fn new(proto_network: ProtoNetwork) -> Result { let mut typing_context = TypingContext::new(&node_registry::NODE_REGISTRY); typing_context.update(&proto_network)?; let output = proto_network.output; - let tree = BorrowTree::new(proto_network, &typing_context).await?; + let tree = BorrowTree::new(proto_network, &typing_context)?; Ok(Self { tree, @@ -64,7 +64,7 @@ impl DynamicExecutor { /// Updates the existing [`BorrowTree`] to reflect the new [`ProtoNetwork`], reusing nodes where possible. #[cfg_attr(debug_assertions, inline(never))] - pub async fn update(&mut self, proto_network: ProtoNetwork) -> Result { + pub fn update(&mut self, proto_network: ProtoNetwork) -> Result { self.output = proto_network.output; self.typing_context.update(&proto_network).map_err(|e| { // If there is an error then get types that have been resolved before the error @@ -87,11 +87,7 @@ impl DynamicExecutor { (ResolvedDocumentNodeTypesDelta { add, remove: Vec::new() }, e) })?; - let (add, orphaned) = self - .tree - .update(proto_network, &self.typing_context) - .await - .map_err(|e| (ResolvedDocumentNodeTypesDelta::default(), e))?; + let (add, orphaned) = self.tree.update(proto_network, &self.typing_context).map_err(|e| (ResolvedDocumentNodeTypesDelta::default(), e))?; let old_to_remove = core::mem::replace(&mut self.orphaned_nodes, orphaned); let mut remove = Vec::with_capacity(old_to_remove.len() - self.orphaned_nodes.len().min(old_to_remove.len())); for node_id in old_to_remove { @@ -204,23 +200,23 @@ pub struct BorrowTree { } impl BorrowTree { - pub async fn new(proto_network: ProtoNetwork, typing_context: &TypingContext) -> Result { + pub fn new(proto_network: ProtoNetwork, typing_context: &TypingContext) -> Result { let mut nodes = BorrowTree::default(); for (id, node) in proto_network.nodes { - nodes.push_node(id, node, typing_context).await? + nodes.push_node(id, node, typing_context)? } Ok(nodes) } /// Pushes new nodes into the tree and return orphaned nodes - pub async fn update(&mut self, proto_network: ProtoNetwork, typing_context: &TypingContext) -> Result<(Vec, HashSet), GraphErrors> { + pub fn update(&mut self, proto_network: ProtoNetwork, typing_context: &TypingContext) -> Result<(Vec, HashSet), GraphErrors> { let mut old_nodes: HashSet<_> = self.nodes.keys().copied().collect(); let mut new_nodes: Vec<_> = Vec::new(); // TODO: Problem: When a passthrough node is connected directly to an export the first input to the passthrough node is not added to the proto network, while the second input is. This means the primary input does not have a type. for (id, node) in proto_network.nodes { if !self.nodes.contains_key(&id) { new_nodes.push(node.original_location.path.clone().unwrap_or_default().into()); - self.push_node(id, node, typing_context).await?; + self.push_node(id, node, typing_context)?; } else if self.update_source_map(id, typing_context, &node) { new_nodes.push(node.original_location.path.clone().unwrap_or_default().into()); } @@ -293,10 +289,10 @@ impl BorrowTree { /// use interpreted_executor::node_registry; /// /// - /// async fn example() -> Result<(), GraphErrors> { + /// fn example() -> Result<(), GraphErrors> { /// let (proto_network, node_id, proto_node) = ProtoNetwork::example(); /// let typing_context = TypingContext::new(&node_registry::NODE_REGISTRY); - /// let mut borrow_tree = BorrowTree::new(proto_network, &typing_context).await?; + /// let mut borrow_tree = BorrowTree::new(proto_network, &typing_context)?; /// /// // Assert that the node exists in the BorrowTree /// assert!(borrow_tree.get(node_id).is_some(), "Node should exist before removal"); @@ -393,7 +389,7 @@ impl BorrowTree { /// - `Nodes`: Constructs a node using other nodes as dependencies. /// - Uses the constructor function from the `typing_context` for `Nodes` construction arguments. /// - Returns an error if no constructor is found for the given node ID. - async fn push_node(&mut self, id: NodeId, proto_node: ProtoNode, typing_context: &TypingContext) -> Result<(), GraphErrors> { + fn push_node(&mut self, id: NodeId, proto_node: ProtoNode, typing_context: &TypingContext) -> Result<(), GraphErrors> { self.update_source_map(id, typing_context, &proto_node); let path = proto_node.original_location.path.clone().unwrap_or_default(); @@ -421,13 +417,13 @@ impl BorrowTree { let adapter_constructor = typing_context .adapter_constructor(&promotion.adapter_identifier()) .ok_or_else(|| vec![GraphError::new(&proto_node, GraphErrorType::NoConstructor)])?; - let adapter = adapter_constructor(vec![construction_nodes[*argument_index].clone()]).await; + let adapter = adapter_constructor(vec![construction_nodes[*argument_index].clone()]); construction_nodes[*argument_index] = NodeContainer::new(adapter); } } let constructor = typing_context.constructor(id).ok_or_else(|| vec![GraphError::new(&proto_node, GraphErrorType::NoConstructor)])?; - let node = constructor(construction_nodes).await; + let node = constructor(construction_nodes); let node = NodeContainer::new(node); self.store_node(node, id, path.into()); } diff --git a/node-graph/libraries/core-types/src/registry.rs b/node-graph/libraries/core-types/src/registry.rs index 2026aaff6d0..2e52c5bd0bf 100644 --- a/node-graph/libraries/core-types/src/registry.rs +++ b/node-graph/libraries/core-types/src/registry.rs @@ -90,7 +90,7 @@ pub type TypeErasedPinned<'n> = Pin>>; pub type SharedNodeContainer = std::sync::Arc; -pub type NodeConstructor = fn(Vec) -> DynFuture<'static, TypeErasedBox<'static>>; +pub type NodeConstructor = fn(Vec) -> TypeErasedBox<'static>; #[derive(Clone)] pub struct NodeContainer { diff --git a/node-graph/node-macro/src/codegen.rs b/node-graph/node-macro/src/codegen.rs index 6f7ce00dd66..54c317d772d 100644 --- a/node-graph/node-macro/src/codegen.rs +++ b/node-graph/node-macro/src/codegen.rs @@ -1238,19 +1238,15 @@ fn generate_register_node_impl( constructors.push(quote!( ( |args| { - Box::pin(async move { - #(#temp_constructors;)* - let node = #variant_struct_name::new(#(#field_names,)*); - // try polling futures - let any: DynAnyNode<#input_type, _, _> = DynAnyNode::new(node); - Box::new(any) as TypeErasedBox<'_> - }) + #(#temp_constructors;)* + let node = #variant_struct_name::new(#(#field_names,)*); + let any: DynAnyNode<#input_type, _, _> = DynAnyNode::new(node); + Box::new(any) as TypeErasedBox<'_> }, { let node = #variant_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 - } ) )); From c7eb37a5df1e5a66147726bb0e606528c8df43e3 Mon Sep 17 00:00:00 2001 From: Timon Date: Tue, 29 Sep 2026 14:13:46 +0000 Subject: [PATCH 09/54] Remove block_on around node construction --- Cargo.lock | 1 - editor/src/node_graph_executor/runtime.rs | 8 +-- node-graph/graphene-cli/Cargo.toml | 1 - node-graph/graphene-cli/src/main.rs | 3 +- .../benches/benchmark_util.rs | 3 +- .../benches/run_demo_art_criterion.rs | 6 +- .../benches/update_executor.rs | 4 +- .../benches/update_executor_gungraun.rs | 4 +- .../src/dynamic_executor/test.rs | 55 +++++++-------- node-graph/interpreted-executor/src/lib.rs | 3 +- .../interpreted-executor/src/node_registry.rs | 70 ++++++++----------- 11 files changed, 69 insertions(+), 89 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4e9c1bd59a5..c172081f4a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2110,7 +2110,6 @@ dependencies = [ "document-container", "document-format", "fern", - "futures", "graph-craft", "graphene-std", "image", diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index 50832f2b97f..30d3fcf5b33 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -182,7 +182,7 @@ impl NodeRuntime { .into(); if let Some(graph) = self.old_graph.clone() { // We ignore this result as compilation errors should have been reported in an earlier iteration - let _ = self.update_network(graph).await; + let _ = self.update_network(graph); } } GraphRuntimeRequest::GraphUpdate(GraphUpdate { @@ -197,7 +197,7 @@ impl NodeRuntime { self.resources = resources; self.node_graph_errors.clear(); - let result = self.update_network(network).await; + let result = self.update_network(network); let node_graph_errors = self.node_graph_errors.clone(); self.update_thumbnails = true; @@ -323,7 +323,7 @@ impl NodeRuntime { None } - async fn update_network(&mut self, graph: NodeNetwork) -> Result { + fn update_network(&mut self, graph: NodeNetwork) -> Result { let mut scoped_network = wrap_network_in_scope(graph, self.editor_api.clone()); if let Err(e) = self.preprocessor.preprocess(&mut scoped_network, &|resource_id| self.resources.hash(&resource_id)) { @@ -346,7 +346,7 @@ impl NodeRuntime { .collect::>(); assert_ne!(proto_network.nodes.len(), 0, "No proto nodes exist?"); - self.executor.update(proto_network).await.map_err(|(types, e)| { + self.executor.update(proto_network).map_err(|(types, e)| { self.node_graph_errors.clone_from(&e); (types, format!("{e:?}")) }) diff --git a/node-graph/graphene-cli/Cargo.toml b/node-graph/graphene-cli/Cargo.toml index 652f635eaf8..67e432f22a5 100644 --- a/node-graph/graphene-cli/Cargo.toml +++ b/node-graph/graphene-cli/Cargo.toml @@ -30,7 +30,6 @@ document-container = { workspace = true, features = ["zip", "xz"] } # Workspace dependencies log = { workspace = true } -futures = { workspace = true } fern = { workspace = true } chrono = { workspace = true } wgpu = { workspace = true } diff --git a/node-graph/graphene-cli/src/main.rs b/node-graph/graphene-cli/src/main.rs index 756419fae93..f403f501c3c 100644 --- a/node-graph/graphene-cli/src/main.rs +++ b/node-graph/graphene-cli/src/main.rs @@ -5,7 +5,6 @@ use document_container::AnyContainer; use document_container::backends::memory::MemoryBackend; use document_format::{GddV1, GddV1Layout}; use fern::colors::{Color, ColoredLevelConfig}; -use futures::executor::block_on; use graph_craft::application_io::EditorPreferences; use graph_craft::application_io::{PlatformApplicationIo, PlatformEditorApi}; use graph_craft::document::*; @@ -276,6 +275,6 @@ fn compile_graph(network: NodeNetwork, editor_api: Arc, gdd: } fn create_executor(proto_network: ProtoNetwork) -> Result> { - let executor = block_on(DynamicExecutor::new(proto_network)).map_err(|errors| errors.iter().map(|e| format!("{e:?}")).reduce(|acc, e| format!("{acc}\n{e}")).unwrap_or_default())?; + let executor = DynamicExecutor::new(proto_network).map_err(|errors| errors.iter().map(|e| format!("{e:?}")).reduce(|acc, e| format!("{acc}\n{e}")).unwrap_or_default())?; Ok(executor) } diff --git a/node-graph/interpreted-executor/benches/benchmark_util.rs b/node-graph/interpreted-executor/benches/benchmark_util.rs index 25c62f55f84..b87f183665f 100644 --- a/node-graph/interpreted-executor/benches/benchmark_util.rs +++ b/node-graph/interpreted-executor/benches/benchmark_util.rs @@ -1,6 +1,5 @@ use criterion::BenchmarkGroup; use criterion::measurement::Measurement; -use futures::executor::block_on; use graph_craft::proto::ProtoNetwork; use graph_craft::util::{DEMO_ART, compile, load_from_name}; use graphene_std::application_io::EditorApi; @@ -14,7 +13,7 @@ pub fn setup_network(name: &str) -> (DynamicExecutor, ProtoNetwork) { let preprocessor = preprocessor::Preprocessor::new(); preprocessor.preprocess(&mut network, &|_| None).unwrap(); let proto_network = compile(network); - let executor = block_on(DynamicExecutor::new(proto_network.clone())).unwrap(); + let executor = DynamicExecutor::new(proto_network.clone()).unwrap(); (executor, proto_network) } diff --git a/node-graph/interpreted-executor/benches/run_demo_art_criterion.rs b/node-graph/interpreted-executor/benches/run_demo_art_criterion.rs index 7f585656653..8334b017bc7 100644 --- a/node-graph/interpreted-executor/benches/run_demo_art_criterion.rs +++ b/node-graph/interpreted-executor/benches/run_demo_art_criterion.rs @@ -11,12 +11,12 @@ fn update_executor(name: &str, c: &mut BenchmarkGroup) { let proto_network = compile(network); let empty = ProtoNetwork::default(); - let executor = futures::executor::block_on(DynamicExecutor::new(empty)).unwrap(); + let executor = DynamicExecutor::new(empty).unwrap(); c.bench_function(name, |b| { b.iter_batched( || (executor.clone(), proto_network.clone()), - |(mut executor, network)| futures::executor::block_on(executor.update(std::hint::black_box(network))), + |(mut executor, network)| executor.update(std::hint::black_box(network)), criterion::BatchSize::SmallInput, ) }); @@ -33,7 +33,7 @@ fn run_once(name: &str, c: &mut BenchmarkGroup) { let network = load_from_name(name); let proto_network = compile(network); - let executor = futures::executor::block_on(DynamicExecutor::new(proto_network)).unwrap(); + let executor = DynamicExecutor::new(proto_network).unwrap(); let footprint = Footprint::default(); c.bench_function(name, |b| b.iter(|| futures::executor::block_on((&executor).execute(footprint)))); diff --git a/node-graph/interpreted-executor/benches/update_executor.rs b/node-graph/interpreted-executor/benches/update_executor.rs index ca5f4c0a4af..081cba763aa 100644 --- a/node-graph/interpreted-executor/benches/update_executor.rs +++ b/node-graph/interpreted-executor/benches/update_executor.rs @@ -13,10 +13,10 @@ fn update_executor(c: &mut Criterion) { || { let (_, proto_network) = setup_network(name); let empty = ProtoNetwork::default(); - let executor = futures::executor::block_on(DynamicExecutor::new(empty)).unwrap(); + let executor = DynamicExecutor::new(empty).unwrap(); (executor, proto_network) }, - |(mut executor, network)| futures::executor::block_on(executor.update(std::hint::black_box(network))), + |(mut executor, network)| executor.update(std::hint::black_box(network)), criterion::BatchSize::SmallInput, ) }); diff --git a/node-graph/interpreted-executor/benches/update_executor_gungraun.rs b/node-graph/interpreted-executor/benches/update_executor_gungraun.rs index 91017ee3b86..42dbbe508a9 100644 --- a/node-graph/interpreted-executor/benches/update_executor_gungraun.rs +++ b/node-graph/interpreted-executor/benches/update_executor_gungraun.rs @@ -9,7 +9,7 @@ use std::hint::black_box; fn setup_update_executor(name: &str) -> (DynamicExecutor, ProtoNetwork) { let (_, proto_network) = setup_network(name); let empty = ProtoNetwork::default(); - let executor = futures::executor::block_on(DynamicExecutor::new(empty)).unwrap(); + let executor = DynamicExecutor::new(empty).unwrap(); (executor, proto_network) } @@ -17,7 +17,7 @@ fn setup_update_executor(name: &str) -> (DynamicExecutor, ProtoNetwork) { #[benches::with_setup(args = ["isometric-fountain", "painted-dreams", "procedural-string-lights", "parametric-dunescape", "red-dress", "valley-of-spires"], setup = setup_update_executor)] pub fn update_executor(setup: (DynamicExecutor, ProtoNetwork)) -> DynamicExecutor { let (mut executor, network) = setup; - let _ = black_box(futures::executor::block_on(executor.update(black_box(network)))); + let _ = black_box(executor.update(black_box(network))); // Return the executor so its teardown happens outside the measured section executor diff --git a/node-graph/interpreted-executor/src/dynamic_executor/test.rs b/node-graph/interpreted-executor/src/dynamic_executor/test.rs index 1b90375d533..bdc6ba7a5f4 100644 --- a/node-graph/interpreted-executor/src/dynamic_executor/test.rs +++ b/node-graph/interpreted-executor/src/dynamic_executor/test.rs @@ -11,8 +11,7 @@ fn push_node_sync() { let mut tree = BorrowTree::default(); let val_1_protonode = ProtoNode::value(ConstructionArgs::Value(TaggedValue::Integer(2_i64).into()), vec![]); let context = TypingContext::default(); - let future = tree.push_node(NodeId(0), val_1_protonode, &context); - futures::executor::block_on(future).unwrap(); + tree.push_node(NodeId(0), val_1_protonode, &context).unwrap(); let _node = tree.get(NodeId(0)).unwrap(); let result: Option> = futures::executor::block_on(tree.eval(NodeId(0), ())); assert_eq!(result.map(|item| *item.element()), Some(2_i64)); @@ -36,7 +35,7 @@ fn compile_bounding_box_network(content: TaggedValue) -> BorrowTree { let network = bounding_box_network(content); let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY); typing_context.update(&network).expect("The network should resolve against exactly one registered wire variant"); - futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The resolved variant's constructor should instantiate") + BorrowTree::new(network, &typing_context).expect("The resolved variant's constructor should instantiate") } #[test] @@ -67,7 +66,7 @@ fn item_wire_promotes_to_list_connector() { let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY); typing_context.update(&network).expect("An Item wire should resolve a List connector via promotion"); assert!(typing_context.promotions(NodeId(1)).is_some(), "The typing pass should record the promotion"); - let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The promotion adapter should instantiate"); + let tree = BorrowTree::new(network, &typing_context).expect("The promotion adapter should instantiate"); let context: Context = None; let result: Option> = futures::executor::block_on(tree.eval(NodeId(1), context)); @@ -97,7 +96,7 @@ fn rank_0_content_promotes_through_the_layer_coercion_path() { typing_context.update(&network).expect("A rank-0 content wire should resolve the layer coercion path via promotion"); assert!(typing_context.promotions(NodeId(1)).is_some(), "The rank-0 content should be raised at Into Group's List connector"); assert!(typing_context.promotions(NodeId(3)).is_some(), "The grouped Item should be raised at Extend's List connector"); - let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The promotion adapters should instantiate"); + let tree = BorrowTree::new(network, &typing_context).expect("The promotion adapters should instantiate"); let context: Context = None; let result: Option> = futures::executor::block_on(tree.eval(NodeId(3), context)); @@ -131,7 +130,7 @@ fn mixed_rank_connectors_resolve_via_promotion() { .update(&network) .expect("A List primary with an Item parameter should resolve the mapped variant via promotion"); assert!(typing_context.promotions(NodeId(3)).is_some(), "The Item distance should be marked for promotion"); - let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("Construction should wrap the promoted argument"); + let tree = BorrowTree::new(network, &typing_context).expect("Construction should wrap the promoted argument"); let context: Context = None; let result: Option> = futures::executor::block_on(tree.eval(NodeId(3), context)); @@ -144,7 +143,7 @@ fn all_item_connectors_resolve_without_promotion() { let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY); typing_context.update(&network).expect("All-Item connectors should resolve the rank-0 variant exactly"); assert!(typing_context.promotions(NodeId(3)).is_none(), "No promotion should be needed at rank 0"); - let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The rank-0 variant should instantiate"); + let tree = BorrowTree::new(network, &typing_context).expect("The rank-0 variant should instantiate"); let context: Context = None; let result: Option> = futures::executor::block_on(tree.eval(NodeId(3), context)); @@ -192,7 +191,7 @@ fn transform_composes_onto_item_wire() { let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY); typing_context.update(&network).expect("Transform should resolve its rank-0 variant"); assert!(typing_context.promotions(output).is_none(), "All-Item connectors should need no promotion"); - let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("Transform's rank-0 variant should instantiate"); + let tree = BorrowTree::new(network, &typing_context).expect("Transform's rank-0 variant should instantiate"); let context: Context = None; let result: Option> = futures::executor::block_on(tree.eval(output, context)); @@ -212,7 +211,7 @@ fn transform_broadcasts_item_content_across_a_framed_parameter() { .update(&network) .expect("A framed rotation should resolve the mapped variant via promotion of the other connectors"); assert!(typing_context.promotions(output).is_some(), "The Item-typed connectors should be raised into the frame"); - let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The mapped variant should instantiate"); + let tree = BorrowTree::new(network, &typing_context).expect("The mapped variant should instantiate"); let context: Context = None; let result: Option> = futures::executor::block_on(tree.eval(output, context)); @@ -245,7 +244,7 @@ fn generator_frames_over_a_list_parameter() { let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY); typing_context.update(&network).expect("A List radius should resolve Circle's mapped generator variant"); - let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The mapped generator variant should instantiate"); + let tree = BorrowTree::new(network, &typing_context).expect("The mapped generator variant should instantiate"); let context: Context = None; let result: Option> = futures::executor::block_on(tree.eval(NodeId(3), context)); @@ -315,7 +314,7 @@ fn value_wires_materialize_as_items_at_resolution() { let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY); typing_context.update(&network).expect("Value wires should materialize as Items and resolve the all-Item variant"); assert!(typing_context.promotions(NodeId(5)).is_none(), "Already-Item value wires should need no promotion"); - let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The all-Item variant should instantiate"); + let tree = BorrowTree::new(network, &typing_context).expect("The all-Item variant should instantiate"); let context: Context = None; let result: Option> = futures::executor::block_on(tree.eval(NodeId(5), context)); @@ -339,7 +338,7 @@ fn position_value_converts_through_the_vector_input_adapter() { }; let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY); typing_context.update(&network).expect("An Item wire should resolve the adapter's element conversion row"); - let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The conversion constructor should instantiate"); + let tree = BorrowTree::new(network, &typing_context).expect("The conversion constructor should instantiate"); let context: Context = None; let result: Option> = futures::executor::block_on(tree.eval(NodeId(1), context)); @@ -364,7 +363,7 @@ fn position_value_raises_into_the_into_group_reducer() { let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY); typing_context.update(&network).expect("An Item wire should raise into Into Group's List row"); assert!(typing_context.promotions(NodeId(1)).is_some(), "The rank-0 position should be raised at Into Group's List connector"); - let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The reducer constructor should instantiate"); + let tree = BorrowTree::new(network, &typing_context).expect("The reducer constructor should instantiate"); let context: Context = None; let result: Option> = futures::executor::block_on(tree.eval(NodeId(1), context)); @@ -398,7 +397,7 @@ fn color_list_wraps_through_the_colors_to_gradient_node() { }; let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY); typing_context.update(&network).expect("A List wire should embed into the node's List connector"); - let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The node constructor should instantiate"); + let tree = BorrowTree::new(network, &typing_context).expect("The node constructor should instantiate"); let context: Context = None; let result: Option> = futures::executor::block_on(tree.eval(NodeId(3), context)); @@ -443,7 +442,7 @@ fn as_graphic_converts_a_vector_list_element_wise() { typing_context .update(&network) .expect("A List wire should resolve As Graphic's mapped variant through the embedding adapter"); - let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The mapped constructor should instantiate"); + let tree = BorrowTree::new(network, &typing_context).expect("The mapped constructor should instantiate"); let context: Context = None; let result: Option> = futures::executor::block_on(tree.eval(NodeId(5), context)); @@ -479,7 +478,7 @@ fn gradient_value_embeds_through_the_graphic_input_adapter() { }; let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY); typing_context.update(&network).expect("An Item wire should resolve the adapter's embedding row"); - let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The embedding constructor should instantiate"); + let tree = BorrowTree::new(network, &typing_context).expect("The embedding constructor should instantiate"); let context: Context = None; let result: Option> = futures::executor::block_on(tree.eval(NodeId(1), context)); @@ -509,7 +508,7 @@ fn number_value_splats_through_the_vec2_input_adapter() { }; let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY); typing_context.update(&network).expect("An f64 wire should resolve the adapter's splat conversion row"); - let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The splat constructor should instantiate"); + let tree = BorrowTree::new(network, &typing_context).expect("The splat constructor should instantiate"); let context: Context = None; let result: Option> = futures::executor::block_on(tree.eval(NodeId(1), context)); @@ -531,7 +530,7 @@ fn number_value_formats_through_the_string_input_adapter() { }; let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY); typing_context.update(&network).expect("An f64 wire should resolve the adapter's formatting conversion row"); - let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The formatting constructor should instantiate"); + let tree = BorrowTree::new(network, &typing_context).expect("The formatting constructor should instantiate"); let context: Context = None; let result: Option> = futures::executor::block_on(tree.eval(NodeId(1), context)); @@ -554,7 +553,7 @@ fn bool_value_embeds_through_the_number_input_adapter() { }; let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY); typing_context.update(&network).expect("A bool wire should resolve the adapter's embedding conversion row"); - let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The embedding constructor should instantiate"); + let tree = BorrowTree::new(network, &typing_context).expect("The embedding constructor should instantiate"); let context: Context = None; let result: Option> = futures::executor::block_on(tree.eval(NodeId(1), context)); @@ -578,7 +577,7 @@ fn bool_value_formats_through_the_string_input_adapter() { }; let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY); typing_context.update(&network).expect("A bool wire should resolve the adapter's formatting conversion row"); - let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The formatting constructor should instantiate"); + let tree = BorrowTree::new(network, &typing_context).expect("The formatting constructor should instantiate"); let context: Context = None; let result: Option> = futures::executor::block_on(tree.eval(NodeId(1), context)); @@ -603,7 +602,7 @@ fn list_wire_erases_through_the_list_dyn_input_adapter() { }; let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY); typing_context.update(&network).expect("A List wire should resolve the ListDyn erasure row"); - let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The erasure constructor should instantiate"); + let tree = BorrowTree::new(network, &typing_context).expect("The erasure constructor should instantiate"); let context: Context = None; let result: Option = futures::executor::block_on(tree.eval(NodeId(1), context)); @@ -625,7 +624,7 @@ fn value_wire_passes_through_the_input_adapter_as_item() { }; let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY); typing_context.update(&network).expect("An f64 value's Item wire should resolve the adapter's passthrough row"); - let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The passthrough constructor should instantiate"); + let tree = BorrowTree::new(network, &typing_context).expect("The passthrough constructor should instantiate"); let context: Context = None; let result: Option> = futures::executor::block_on(tree.eval(NodeId(1), context)); @@ -651,7 +650,7 @@ fn modification_value_rides_the_item_wire_through_its_input_adapter() { }; let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY); typing_context.update(&network).expect("A modification value's Item wire should resolve the adapter's passthrough row"); - let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The passthrough constructor should instantiate"); + let tree = BorrowTree::new(network, &typing_context).expect("The passthrough constructor should instantiate"); let context: Context = None; let result: Option>> = futures::executor::block_on(tree.eval(NodeId(1), context)); @@ -675,7 +674,7 @@ fn item_wire_boxes_into_the_attribute_value_connector() { let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY); typing_context.update(&network).expect("An Item wire should resolve the attribute value boxing row"); assert!(typing_context.promotions(NodeId(1)).is_none(), "The already-Item value wire should need no promotion"); - let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The boxing constructor should instantiate"); + let tree = BorrowTree::new(network, &typing_context).expect("The boxing constructor should instantiate"); let context: Context = None; let result: Option> = futures::executor::block_on(tree.eval(NodeId(1), context)); @@ -733,7 +732,7 @@ fn expander_flattens_under_the_frame() { .update(&network) .expect("All-Item connectors should resolve the expander's direct `Item -> List` variant"); assert!(typing_context.promotions(output).is_none(), "No promotion should be needed when every connector is already an Item"); - let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The expander variant should instantiate"); + let tree = BorrowTree::new(network, &typing_context).expect("The expander variant should instantiate"); let context: Context = None; let result: Option> = futures::executor::block_on(tree.eval(output, context)); @@ -779,7 +778,7 @@ fn whole_list_switches_as_one_bundle() { .count(); assert_eq!(branch_bundles, 2, "Both branches should bundle their whole list into one opaque cell"); - let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The bundle, wrap, and unbundle adapters should instantiate"); + let tree = BorrowTree::new(network, &typing_context).expect("The bundle, wrap, and unbundle adapters should instantiate"); let context: Context = None; let result: Option> = futures::executor::block_on(tree.eval(NodeId(4), context)); let list = result.expect("The whole stack should round-trip through the bundle switch back to a flat List"); @@ -814,7 +813,7 @@ fn a_bundle_unbundles_into_a_list_connector() { "The base connector should unbundle the whole list" ); - let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The unbundle adapter should instantiate"); + let tree = BorrowTree::new(network, &typing_context).expect("The unbundle adapter should instantiate"); let context: Context = None; let result: Option> = futures::executor::block_on(tree.eval(NodeId(3), context)); assert!(result.is_some(), "The unbundled stack should flow into Extend as a List"); @@ -856,7 +855,7 @@ fn a_whole_list_of_scalars_switches_as_one_bundle() { .count(); assert_eq!(branch_bundles, 2, "Both scalar-list branches should bundle into one opaque cell"); - let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The bundle, wrap, and unbundle adapters should instantiate"); + let tree = BorrowTree::new(network, &typing_context).expect("The bundle, wrap, and unbundle adapters should instantiate"); let context: Context = None; let result: Option> = futures::executor::block_on(tree.eval(NodeId(4), context)); let list = result.expect("The whole scalar list should round-trip through the bundle switch"); diff --git a/node-graph/interpreted-executor/src/lib.rs b/node-graph/interpreted-executor/src/lib.rs index 68081de5a89..44fca1526ac 100644 --- a/node-graph/interpreted-executor/src/lib.rs +++ b/node-graph/interpreted-executor/src/lib.rs @@ -5,7 +5,6 @@ pub mod util; #[cfg(test)] mod tests { use core_types::*; - use futures::executor::block_on; use graphene_core::ops::passthrough; #[test] @@ -47,6 +46,6 @@ mod tests { let compiler = Compiler {}; let protograph = compiler.compile_single(network).expect("Graph should be generated"); - let _exec = block_on(DynamicExecutor::new(protograph)).map(|_e| panic!("The network should not type check ")).unwrap_err(); + let _exec = DynamicExecutor::new(protograph).map(|_e| panic!("The network should not type check ")).unwrap_err(); } } diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index 3aa5f559565..54677797b53 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -158,11 +158,9 @@ fn node_registry() -> HashMap"]), |mut args| { - Box::pin(async move { - let node = graphene_core::ops::PassthroughNode::new(graphene_std::any::downcast_node::(args.pop().unwrap())); - let any: DynAnyNode = graphene_std::any::DynAnyNode::new(node); - Box::new(any) as TypeErasedBox - }) + let node = graphene_core::ops::PassthroughNode::new(graphene_std::any::downcast_node::(args.pop().unwrap())); + let any: DynAnyNode = graphene_std::any::DynAnyNode::new(node); + Box::new(any) as TypeErasedBox }, { let node = graphene_core::ops::PassthroughNode::new(graphene_std::any::PanicNode:: + Send>>>::new()); @@ -176,14 +174,12 @@ fn node_registry() -> HashMap"]), |mut args| { - Box::pin(async move { - let node = graphene_core::ops::IntoNode::new( - graphene_std::any::downcast_node::(args.pop().unwrap()), - graphene_std::any::FutureWrapperNode::new(graphene_std::value::ClonedNode::new(std::marker::PhantomData::<$to>)), - ); - let any: DynAnyNode = graphene_std::any::DynAnyNode::new(node); - Box::new(any) as TypeErasedBox - }) + let node = graphene_core::ops::IntoNode::new( + graphene_std::any::downcast_node::(args.pop().unwrap()), + graphene_std::any::FutureWrapperNode::new(graphene_std::value::ClonedNode::new(std::marker::PhantomData::<$to>)), + ); + let any: DynAnyNode = graphene_std::any::DynAnyNode::new(node); + Box::new(any) as TypeErasedBox }, { let node = graphene_core::ops::IntoNode::new( @@ -211,14 +207,12 @@ fn node_registry() -> HashMap"]), |mut args| { - Box::pin(async move { - let node = graphene_core::ops::$node::new( - graphene_std::any::downcast_node::(args.pop().unwrap()), - graphene_std::any::FutureWrapperNode::new(graphene_std::value::ClonedNode::new(std::marker::PhantomData::<$element>)), - ); - let any: DynAnyNode = graphene_std::any::DynAnyNode::new(node); - Box::new(any) as TypeErasedBox - }) + let node = graphene_core::ops::$node::new( + graphene_std::any::downcast_node::(args.pop().unwrap()), + graphene_std::any::FutureWrapperNode::new(graphene_std::value::ClonedNode::new(std::marker::PhantomData::<$element>)), + ); + let any: DynAnyNode = graphene_std::any::DynAnyNode::new(node); + Box::new(any) as TypeErasedBox }, { let node = graphene_core::ops::$node::new( @@ -238,11 +232,9 @@ fn node_registry() -> HashMap"]), |mut args| { - Box::pin(async move { - let node = graphene_core::ops::ItemToListNode::new(graphene_std::any::downcast_node::>(args.pop().unwrap())); - let any: DynAnyNode, _> = graphene_std::any::DynAnyNode::new(node); - Box::new(any) as TypeErasedBox - }) + let node = graphene_core::ops::ItemToListNode::new(graphene_std::any::downcast_node::>(args.pop().unwrap())); + let any: DynAnyNode, _> = graphene_std::any::DynAnyNode::new(node); + Box::new(any) as TypeErasedBox }, { let node = graphene_core::ops::ItemToListNode::new(graphene_std::any::PanicNode::< @@ -262,11 +254,9 @@ fn node_registry() -> HashMap"]), |mut args| { - Box::pin(async move { - let node = graphene_core::ops::BundleNode::new(graphene_std::any::downcast_node::>(args.pop().unwrap())); - let any: DynAnyNode>, _> = graphene_std::any::DynAnyNode::new(node); - Box::new(any) as TypeErasedBox - }) + let node = graphene_core::ops::BundleNode::new(graphene_std::any::downcast_node::>(args.pop().unwrap())); + let any: DynAnyNode>, _> = graphene_std::any::DynAnyNode::new(node); + Box::new(any) as TypeErasedBox }, { let node = graphene_core::ops::BundleNode::new(graphene_std::any::PanicNode::< @@ -286,11 +276,9 @@ fn node_registry() -> HashMap"]), |mut args| { - Box::pin(async move { - let node = graphene_core::ops::UnbundleNode::new(graphene_std::any::downcast_node::>>(args.pop().unwrap())); - let any: DynAnyNode, _> = graphene_std::any::DynAnyNode::new(node); - Box::new(any) as TypeErasedBox - }) + let node = graphene_core::ops::UnbundleNode::new(graphene_std::any::downcast_node::>>(args.pop().unwrap())); + let any: DynAnyNode, _> = graphene_std::any::DynAnyNode::new(node); + Box::new(any) as TypeErasedBox }, { let node = graphene_core::ops::UnbundleNode::new(graphene_std::any::PanicNode::< @@ -645,12 +633,10 @@ mod node_registry_macros { ( $identifier, |mut args| { - Box::pin(async move { - args.reverse(); - let node = <$path>::new($(graphene_std::any::downcast_node::<$arg, $type>(args.pop().expect("Not enough arguments provided to construct node"))),*); - let any: DynAnyNode<$input, _, _> = graphene_std::any::DynAnyNode::new(node); - Box::new(any) as TypeErasedBox - }) + args.reverse(); + let node = <$path>::new($(graphene_std::any::downcast_node::<$arg, $type>(args.pop().expect("Not enough arguments provided to construct node"))),*); + let any: DynAnyNode<$input, _, _> = graphene_std::any::DynAnyNode::new(node); + Box::new(any) as TypeErasedBox }, { let node = <$path>::new($( From b725e05ff36e7dceadf21106c7a6d3510760826e Mon Sep 17 00:00:00 2001 From: Timon Date: Tue, 29 Sep 2026 15:19:18 +0000 Subject: [PATCH 10/54] Move GPU readback into ConvertAsync --- editor/src/node_graph_executor/runtime.rs | 6 +++--- node-graph/graphene-cli/src/export.rs | 6 +++--- node-graph/libraries/core-types/src/ops.rs | 6 ++++++ .../wgpu-executor/src/texture_conversion.rs | 14 +++++++------- 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index 30d3fcf5b33..d03291f7ef6 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -12,7 +12,7 @@ use graphene_std::application_io::{ApplicationIo, ExportFormat, RenderConfig, Te use graphene_std::bounds::RenderBoundingBox; use graphene_std::list::{Item, List}; use graphene_std::memo::IORecord; -use graphene_std::ops::Convert; +use graphene_std::ops::ConvertAsync; #[cfg(all(target_family = "wasm", feature = "gpu", feature = "wasm"))] use graphene_std::platform_application_io::canvas_utils::{Canvas, CanvasSurface, CanvasSurfaceHandle}; use graphene_std::raster_types::Raster; @@ -238,7 +238,7 @@ impl NodeRuntime { .gpu_executor() .expect("GPU executor should be available when we receive a texture"); - let raster_cpu = Raster::new_gpu(texture).convert(Footprint::BOUNDLESS, executor).await; + let raster_cpu = Raster::new_gpu(texture).convert(Footprint::BOUNDLESS, executor.clone()).await; let (data, width, height) = raster_cpu.to_flat_u8(); @@ -262,7 +262,7 @@ impl NodeRuntime { .gpu_executor() .expect("GPU executor should be available when we receive a texture"); - let raster_cpu = Raster::new_gpu(texture).convert(Footprint::BOUNDLESS, executor).await; + let raster_cpu = Raster::new_gpu(texture).convert(Footprint::BOUNDLESS, executor.clone()).await; self.sender.send(NodeGraphUpdate::EyedropperPreview(raster_cpu)).expect("Failed to send response"); continue; diff --git a/node-graph/graphene-cli/src/export.rs b/node-graph/graphene-cli/src/export.rs index c1d0da0de44..602f5cf2d1e 100644 --- a/node-graph/graphene-cli/src/export.rs +++ b/node-graph/graphene-cli/src/export.rs @@ -1,7 +1,7 @@ use graph_craft::document::value::{RenderOutputType, TaggedValue, UVec2}; use graph_craft::graphene_compiler::Executor; use graphene_std::application_io::{ExportFormat, RenderConfig, TimingInformation}; -use graphene_std::core_types::ops::Convert; +use graphene_std::core_types::ops::ConvertAsync; use graphene_std::core_types::transform::Footprint; use graphene_std::raster_types::{CPU, GPU, Raster}; use interpreted_executor::dynamic_executor::DynamicExecutor; @@ -70,7 +70,7 @@ pub async fn export_document( RenderOutputType::Texture(texture) => { // Convert GPU texture to CPU buffer let gpu_raster = Raster::::new_gpu(texture); - let cpu_raster: Raster = gpu_raster.convert(Footprint::BOUNDLESS, wgpu_executor).await; + let cpu_raster: Raster = gpu_raster.convert(Footprint::BOUNDLESS, wgpu_executor.clone()).await; let (data, width, height) = cpu_raster.to_flat_u8(); // Encode and write raster image @@ -202,7 +202,7 @@ pub async fn export_gif( TaggedValue::RenderOutput(output) => match output.data { RenderOutputType::Texture(texture) => { let gpu_raster = Raster::::new_gpu(texture); - let cpu_raster: Raster = gpu_raster.convert(Footprint::BOUNDLESS, wgpu_executor).await; + let cpu_raster: Raster = gpu_raster.convert(Footprint::BOUNDLESS, wgpu_executor.clone()).await; cpu_raster.to_flat_u8() } RenderOutputType::Buffer { data, width, height } => (data, width, height), diff --git a/node-graph/libraries/core-types/src/ops.rs b/node-graph/libraries/core-types/src/ops.rs index 17db5a7f70b..cbe7555ea83 100644 --- a/node-graph/libraries/core-types/src/ops.rs +++ b/node-graph/libraries/core-types/src/ops.rs @@ -42,6 +42,12 @@ pub trait Convert: Sized { fn convert(self, footprint: Footprint, converter: C) -> impl Future + Send; } +/// Asynchronous version of [`Convert`] +pub trait ConvertAsync: Sized { + #[must_use] + fn convert(self, footprint: Footprint, converter: C) -> impl std::future::Future + Send + 'static; +} + /// Implements the [`Convert`] trait for formatting a type into a `String` via [`ToString`]. macro_rules! impl_convert_to_string { ($($from:ty),* $(,)?) => { diff --git a/node-graph/libraries/wgpu-executor/src/texture_conversion.rs b/node-graph/libraries/wgpu-executor/src/texture_conversion.rs index 93fb12da34d..8a2e8f6de1d 100644 --- a/node-graph/libraries/wgpu-executor/src/texture_conversion.rs +++ b/node-graph/libraries/wgpu-executor/src/texture_conversion.rs @@ -2,7 +2,7 @@ use crate::{Buffer, WgpuExecutor}; use core_types::Color; use core_types::color::SRGBA8; use core_types::list::{Item, List}; -use core_types::ops::Convert; +use core_types::ops::{Convert, ConvertAsync}; use core_types::transform::Footprint; use raster_types::Image; use raster_types::{CPU, GPU, Raster, Texture}; @@ -177,8 +177,8 @@ impl<'i> Convert>, &'i WgpuExecutor> for List> { } /// Converts a `List>` to `List>` by downloading texture data in one go then asynchronously maps all buffers and processes the results. -impl<'i> Convert>, &'i WgpuExecutor> for List> { - async fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> List> { +impl ConvertAsync>, WgpuExecutor> for List> { + async fn convert(self, _: Footprint, executor: WgpuExecutor) -> List> { let device = &executor.context().device; let queue = &executor.context().queue; @@ -191,7 +191,7 @@ impl<'i> Convert>, &'i WgpuExecutor> for List> { for row in self { let (element, attributes) = row.into_parts(); - converters.push(RasterGpuToRasterCpuConverter::new(executor, &mut encoder, element)); + converters.push(RasterGpuToRasterCpuConverter::new(&executor, &mut encoder, element)); rows_meta.push(Item::from_parts((), attributes)); } @@ -219,8 +219,8 @@ impl<'i> Convert>, &'i WgpuExecutor> for List> { } /// Converts single GPU raster to CPU by downloading texture data -impl<'i> Convert, &'i WgpuExecutor> for Raster { - async fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> Raster { +impl ConvertAsync, WgpuExecutor> for Raster { + async fn convert(self, _: Footprint, executor: WgpuExecutor) -> Raster { let device = &executor.context().device; let queue = &executor.context().queue; @@ -228,7 +228,7 @@ impl<'i> Convert, &'i WgpuExecutor> for Raster { label: Some("single_texture_download_encoder"), }); - let converter = RasterGpuToRasterCpuConverter::new(executor, &mut encoder, self); + let converter = RasterGpuToRasterCpuConverter::new(&executor, &mut encoder, self); queue.submit([encoder.finish()]); From 12c163831e376ea06146f4bb73b06eddd8573190 Mon Sep 17 00:00:00 2001 From: Timon Date: Tue, 29 Sep 2026 15:21:32 +0000 Subject: [PATCH 11/54] Make Convert sync --- Cargo.lock | 1 - node-graph/libraries/core-types/Cargo.toml | 1 - node-graph/libraries/core-types/src/ops.rs | 27 +++++++++---------- .../wgpu-executor/src/texture_conversion.rs | 8 +++--- node-graph/nodes/gcore/src/ops.rs | 4 +-- .../nodes/gstd/src/platform_application_io.rs | 2 +- 6 files changed, 20 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c172081f4a6..f6dcec7e90a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -922,7 +922,6 @@ dependencies = [ "serde_json", "skrifa 0.42.1", "tinyvec", - "tokio", "tsify", "wasm-bindgen", ] diff --git a/node-graph/libraries/core-types/Cargo.toml b/node-graph/libraries/core-types/Cargo.toml index c1e4daec7b5..c6856f2c1ed 100644 --- a/node-graph/libraries/core-types/Cargo.toml +++ b/node-graph/libraries/core-types/Cargo.toml @@ -58,5 +58,4 @@ wasm-bindgen = { workspace = true, optional = true } [dev-dependencies] # Workspace dependencies -tokio = { workspace = true } serde_json = { workspace = true } diff --git a/node-graph/libraries/core-types/src/ops.rs b/node-graph/libraries/core-types/src/ops.rs index cbe7555ea83..5d3cf37fd97 100644 --- a/node-graph/libraries/core-types/src/ops.rs +++ b/node-graph/libraries/core-types/src/ops.rs @@ -2,7 +2,6 @@ use crate::Node; use crate::math::float_noise::round_away_float_noise; use crate::transform::Footprint; use glam::{DAffine2, DVec2}; -use std::future::Future; use std::marker::PhantomData; // Type @@ -39,7 +38,7 @@ 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; } /// Asynchronous version of [`Convert`] @@ -54,7 +53,7 @@ macro_rules! impl_convert_to_string { $( impl Convert for $from { #[inline] - async fn convert(self, _: Footprint, _converter: ()) -> String { + fn convert(self, _: Footprint, _converter: ()) -> String { self.to_string() } } @@ -66,7 +65,7 @@ impl_convert_to_string!(f32, u32, u64, i32, i64, bool, DVec2, DAffine2); // Denoised so 0.1 + 0.2 reaches the string as "0.3" rather than "0.30000000000000004" impl Convert for f64 { #[inline] - async fn convert(self, _: Footprint, _converter: ()) -> String { + fn convert(self, _: Footprint, _converter: ()) -> String { round_away_float_noise(self).to_string() } } @@ -83,14 +82,14 @@ pub trait FromAnchorPosition { 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 } } }; ($from:ty => round $to:ty) => { impl Convert<$to, ()> for $from { - async fn convert(self, _: Footprint, _: ()) -> $to { + fn convert(self, _: Footprint, _: ()) -> $to { self.round() as $to } } @@ -110,7 +109,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) } } @@ -147,7 +146,7 @@ macro_rules! impl_convert_from_bool { ($($to:ty),* $(,)?) => { $( impl Convert<$to, ()> for bool { - async fn convert(self, _: Footprint, _: ()) -> $to { + fn convert(self, _: Footprint, _: ()) -> $to { self as u8 as $to } } @@ -160,12 +159,12 @@ impl_convert_from_bool!(f32, f64, i8, u8, u16, i16, i32, u32, i64, u64, i128, u1 mod tests { use super::*; - #[tokio::test] - async fn a_float_narrows_to_the_nearest_integer() { + #[test] + fn a_float_narrows_to_the_nearest_integer() { let footprint = Footprint::default(); - assert_eq!(Convert::::convert(5.999_999_f64, footprint, ()).await, 6, "float noise rounds up to the whole number it meant"); - assert_eq!(Convert::::convert(2.5_f64, footprint, ()).await, 3, "ties round away from zero"); - assert_eq!(Convert::::convert(-1.5_f64, footprint, ()).await, -2); - assert_eq!(Convert::::convert(7_i64, footprint, ()).await, 7., "an integer widens exactly"); + assert_eq!(Convert::::convert(5.999_999_f64, footprint, ()), 6, "float noise rounds up to the whole number it meant"); + assert_eq!(Convert::::convert(2.5_f64, footprint, ()), 3, "ties round away from zero"); + assert_eq!(Convert::::convert(-1.5_f64, footprint, ()), -2); + assert_eq!(Convert::::convert(7_i64, footprint, ()), 7., "an integer widens exactly"); } } diff --git a/node-graph/libraries/wgpu-executor/src/texture_conversion.rs b/node-graph/libraries/wgpu-executor/src/texture_conversion.rs index 8a2e8f6de1d..8a5bac537d4 100644 --- a/node-graph/libraries/wgpu-executor/src/texture_conversion.rs +++ b/node-graph/libraries/wgpu-executor/src/texture_conversion.rs @@ -134,14 +134,14 @@ impl RasterGpuToRasterCpuConverter { /// Passthrough conversion for GPU `List`s - no conversion needed impl<'i> Convert>, &'i WgpuExecutor> for List> { - async fn convert(self, _: Footprint, _converter: &'i WgpuExecutor) -> List> { + fn convert(self, _: Footprint, _converter: &'i WgpuExecutor) -> List> { self } } /// Converts a `List>` to `List>` by uploading each image to a texture impl<'i> Convert>, &'i WgpuExecutor> for List> { - async fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> List> { + fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> List> { let queue = executor.context().queue.lock(); let list = self .into_iter() @@ -160,7 +160,7 @@ impl<'i> Convert>, &'i WgpuExecutor> for List> { /// Converts single CPU raster to GPU by uploading to texture impl<'i> Convert, &'i WgpuExecutor> for Raster { - async fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> Raster { + fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> Raster { let queue = executor.context().queue.lock(); let texture = upload_to_texture(executor, &queue, &self); @@ -171,7 +171,7 @@ impl<'i> Convert, &'i WgpuExecutor> for Raster { /// Passthrough conversion for CPU `List`s - no conversion needed impl<'i> Convert>, &'i WgpuExecutor> for List> { - async fn convert(self, _: Footprint, _converter: &'i WgpuExecutor) -> List> { + fn convert(self, _: Footprint, _converter: &'i WgpuExecutor) -> List> { self } } diff --git a/node-graph/nodes/gcore/src/ops.rs b/node-graph/nodes/gcore/src/ops.rs index 402f2d7e399..e8326166842 100644 --- a/node-graph/nodes/gcore/src/ops.rs +++ b/node-graph/nodes/gcore/src/ops.rs @@ -96,7 +96,7 @@ async fn convert_item<'i, T: 'i + Send + Convert, E: 'i + Send>(ctx: impl let footprint = *ctx.try_footprint().unwrap_or(&Footprint::DEFAULT); let (value, attributes) = value.into_parts(); - Item::from_parts(value.convert(footprint, ()).await, attributes) + Item::from_parts(value.convert(footprint, ()), attributes) } /// The `List` counterpart of `convert_item`, casting every element to the connector's numeric element type. @@ -107,7 +107,7 @@ async fn convert_list<'i, T: 'i + Send + Convert, E: 'i + Send>(ctx: impl let mut result = List::default(); for item in value.into_iter() { let (value, attributes) = item.into_parts(); - result.push(Item::from_parts(value.convert(footprint, ()).await, attributes)); + result.push(Item::from_parts(value.convert(footprint, ()), attributes)); } result diff --git a/node-graph/nodes/gstd/src/platform_application_io.rs b/node-graph/nodes/gstd/src/platform_application_io.rs index b0447eef635..7db1bf5b61d 100644 --- a/node-graph/nodes/gstd/src/platform_application_io.rs +++ b/node-graph/nodes/gstd/src/platform_application_io.rs @@ -292,5 +292,5 @@ async fn upload_texture<'a: 'n>(_: impl Ctx, content: Item>, #[scope let executor = executor.into_element(); let (raster, attributes) = content.into_parts(); - Item::from_parts(raster.convert(Footprint::DEFAULT, executor).await, attributes) + Item::from_parts(raster.convert(Footprint::DEFAULT, executor), attributes) } From 3f841fe82550f78538b49f36c4f30c1f8b028560 Mon Sep 17 00:00:00 2001 From: Timon Date: Tue, 29 Sep 2026 15:24:29 +0000 Subject: [PATCH 12/54] Make wgpu pipelines and vello rendering sync --- desktop/src/render/state.rs | 2 +- node-graph/libraries/wgpu-executor/src/lib.rs | 7 ++-- .../libraries/wgpu-executor/src/pipeline.rs | 32 ++----------------- .../per_pixel_adjust_runtime.rs | 6 ++-- .../src/shader_nodes/per_pixel_adjust.rs | 4 +-- node-graph/nodes/brush/src/basic_brush/mod.rs | 2 +- .../nodes/brush/src/basic_brush/pipeline.rs | 6 ++-- .../nodes/gstd/src/render_background.rs | 21 ++++++------ node-graph/nodes/gstd/src/render_node.rs | 1 - .../nodes/gstd/src/render_pixel_preview.rs | 19 +++++------ 10 files changed, 33 insertions(+), 67 deletions(-) diff --git a/desktop/src/render/state.rs b/desktop/src/render/state.rs index 3958b8cd3ce..c3395c16a48 100644 --- a/desktop/src/render/state.rs +++ b/desktop/src/render/state.rs @@ -228,7 +228,7 @@ impl RenderState { return; }; let size = glam::UVec2::new(viewport_texture.width(), viewport_texture.height()); - let result = futures::executor::block_on(self.executor.render_vello_scene(&scene, size, &Default::default(), None)); + let result = self.executor.render_vello_scene(&scene, size, &Default::default(), None); match result { Ok(texture) => { self.overlays_texture = Some(texture); diff --git a/node-graph/libraries/wgpu-executor/src/lib.rs b/node-graph/libraries/wgpu-executor/src/lib.rs index e363f31c2e9..94e3e03c000 100644 --- a/node-graph/libraries/wgpu-executor/src/lib.rs +++ b/node-graph/libraries/wgpu-executor/src/lib.rs @@ -10,10 +10,10 @@ use crate::texture_cache::TextureCache; use anyhow::Result; use core_types::Color; use core_types::color::SRGBA8; -use futures::lock::Mutex; use glam::UVec2; use graphene_application_io::{ApplicationIo, EditorApi}; use std::sync::Arc; +use std::sync::Mutex; use vello::{AaConfig, AaSupport, RenderParams, Renderer, RendererOptions, Scene}; use wgpu::util::DeviceExt; use wgpu::{Origin3d, TextureAspect}; @@ -21,7 +21,6 @@ use wgpu::{Origin3d, TextureAspect}; pub use buffer::Buffer; pub use context::Context as WgpuContext; pub use context::ContextBuilder as WgpuContextBuilder; -pub use pipeline::AsyncPipeline as AsyncWgpuPipeline; pub use pipeline::Pipeline as WgpuPipeline; pub use pipeline::PipelineCache as WgpuPipelineCache; pub use raster_types::Texture; @@ -70,7 +69,7 @@ impl<'a, T: ApplicationIo> From<&'a EditorApi> for & } impl WgpuExecutor { - pub async fn render_vello_scene(&self, scene: &Scene, size: UVec2, context: &RenderContext, background: Option) -> Result { + pub fn render_vello_scene(&self, scene: &Scene, size: UVec2, context: &RenderContext, background: Option) -> Result { let texture = self.request_texture(size); let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default()); @@ -84,7 +83,7 @@ impl WgpuExecutor { }; { - let mut renderer = self.inner.vello_renderer.lock().await; + let mut renderer = self.inner.vello_renderer.lock().unwrap(); for (image_brush, texture) in context.resource_overrides.iter() { let texture_view = wgpu::TexelCopyTextureInfoBase { texture: (**texture).clone(), diff --git a/node-graph/libraries/wgpu-executor/src/pipeline.rs b/node-graph/libraries/wgpu-executor/src/pipeline.rs index 281a8a9dfde..031d2b9dda7 100644 --- a/node-graph/libraries/wgpu-executor/src/pipeline.rs +++ b/node-graph/libraries/wgpu-executor/src/pipeline.rs @@ -1,42 +1,16 @@ use dyn_any::DynAny; use std::any::Any; -use std::future::Future; -use std::pin::Pin; use std::sync::{Arc, OnceLock}; use crate::WgpuExecutor; -pub type PipelineFuture<'a, T> = Pin + Send + 'a>>; - pub trait Pipeline: Any + Send + Sync + Sized { type Args<'a>; type Out: Send; fn create(executor: &WgpuExecutor) -> Self; - fn run<'a>(&'a self, executor: &'a WgpuExecutor, args: &'a Self::Args<'_>) -> PipelineFuture<'a, Self::Out>; -} - -pub trait AsyncPipeline: Any + Send + Sync + Sized { - type Args<'a>; - type Out: Send; - - fn create(executor: &WgpuExecutor) -> Self; - - fn run<'a>(&'a self, executor: &'a WgpuExecutor, args: &'a Self::Args<'_>) -> impl Future + Send + 'a; -} - -impl Pipeline for P { - type Args<'a> =

::Args<'a>; - type Out =

::Out; - - fn create(executor: &WgpuExecutor) -> Self { -

::create(executor) - } - - fn run<'a>(&'a self, executor: &'a WgpuExecutor, args: &'a Self::Args<'_>) -> PipelineFuture<'a, Self::Out> { - Box::pin(

::run(self, executor, args)) - } + fn run(&self, executor: &WgpuExecutor, args: &Self::Args<'_>) -> Self::Out; } #[derive(Default, Clone, DynAny)] @@ -51,13 +25,13 @@ impl PipelineCache { self.pipeline.get_or_init(|| Box::new(P::create(executor))); } - pub async fn run(&self, args: &P::Args<'_>) -> P::Out { + pub fn run(&self, args: &P::Args<'_>) -> P::Out { let executor = self.executor.get().expect("PipelineCache not initialized"); let entry = self.pipeline.get().expect("PipelineCache not initialized"); let pipeline = (**entry) .downcast_ref::

() .unwrap_or_else(|| panic!("PipelineCache type mismatch: run::<{}>() but init used a different pipeline type", std::any::type_name::

(),)); - pipeline.run(executor, args).await + pipeline.run(executor, args) } } diff --git a/node-graph/libraries/wgpu-executor/src/shader_runtime/per_pixel_adjust_runtime.rs b/node-graph/libraries/wgpu-executor/src/shader_runtime/per_pixel_adjust_runtime.rs index 761fbcad259..ffb4406efa1 100644 --- a/node-graph/libraries/wgpu-executor/src/shader_runtime/per_pixel_adjust_runtime.rs +++ b/node-graph/libraries/wgpu-executor/src/shader_runtime/per_pixel_adjust_runtime.rs @@ -2,11 +2,11 @@ use crate::shader_runtime::FULLSCREEN_VERTEX_SHADER_NAME; use crate::{Buffer, WgpuContext, WgpuExecutor}; use core_types::list::{Item, List}; use core_types::shaders::buffer_struct::BufferStruct; -use futures::lock::Mutex; use glam::UVec2; use raster_types::{GPU, Raster}; use std::borrow::Cow; use std::collections::HashMap; +use std::sync::Mutex; use wgpu::util::BufferInitDescriptor; use wgpu::{ BindGroupDescriptor, BindGroupEntry, BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingResource, BindingType, BufferBinding, BufferBindingType, BufferUsages, ColorTargetState, Face, @@ -34,8 +34,8 @@ impl PerPixelAdjustShaderRuntime { } impl WgpuExecutor { - pub async fn run_per_pixel_adjust(&self, shaders: &Shaders<'_>, textures: List>, args: Option<&T>) -> List> { - let mut cache = self.inner.shader_runtime.per_pixel_adjust.pipeline_cache.lock().await; + pub fn run_per_pixel_adjust(&self, shaders: &Shaders<'_>, textures: List>, args: Option<&T>) -> List> { + let mut cache = self.inner.shader_runtime.per_pixel_adjust.pipeline_cache.lock().unwrap(); let pipeline = cache .entry(shaders.fragment_shader_name.to_owned()) .or_insert_with(|| PerPixelAdjustGraphicsPipeline::new(self.context(), shaders)); 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 2805d769883..25369a5e323 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 @@ -325,7 +325,7 @@ impl PerPixelAdjustCodegen<'_> { wgsl_shader: crate::WGSL_SHADER, fragment_shader_name: super::#entry_point_name, has_uniform: #has_uniform, - }, #gpu_image, #uniform_buffer).await + }, #gpu_image, #uniform_buffer) } }; @@ -353,7 +353,7 @@ impl PerPixelAdjustCodegen<'_> { }, output_type: raster_gpu, output_element: None, - is_async: true, + is_async: false, fields, body, description: self.parsed.description.clone(), diff --git a/node-graph/nodes/brush/src/basic_brush/mod.rs b/node-graph/nodes/brush/src/basic_brush/mod.rs index a8520b548f2..7b8ca11744d 100644 --- a/node-graph/nodes/brush/src/basic_brush/mod.rs +++ b/node-graph/nodes/brush/src/basic_brush/mod.rs @@ -57,7 +57,7 @@ async fn basic_brush<'a: 'n>( strokes: &strokes, cache: &cache, }; - let Some((texture, transform)) = pipeline.run::(&args).await else { + let Some((texture, transform)) = pipeline.run::(&args) else { return List::new(); }; let raster = Raster::::new_gpu(texture); diff --git a/node-graph/nodes/brush/src/basic_brush/pipeline.rs b/node-graph/nodes/brush/src/basic_brush/pipeline.rs index b103f3fb61a..01430293bbb 100644 --- a/node-graph/nodes/brush/src/basic_brush/pipeline.rs +++ b/node-graph/nodes/brush/src/basic_brush/pipeline.rs @@ -9,7 +9,7 @@ use core_types::Color; use core_types::transform::Footprint; use glam::{DAffine2, UVec2}; use raster_types::Texture; -use wgpu_executor::{AsyncWgpuPipeline, Buffer, WgpuExecutor}; +use wgpu_executor::{Buffer, WgpuExecutor, WgpuPipeline}; pub(super) const DENSITY_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::R16Float; pub(super) const COMPOSITE_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float; @@ -71,7 +71,7 @@ pub struct BasicBrushPipelineArgs<'a> { pub(super) cache: &'a BrushCache, } -impl AsyncWgpuPipeline for BasicBrushPipeline { +impl WgpuPipeline for BasicBrushPipeline { type Args<'a> = BasicBrushPipelineArgs<'a>; type Out = Option<(Texture, DAffine2)>; @@ -85,7 +85,7 @@ impl AsyncWgpuPipeline for BasicBrushPipeline { } } - async fn run<'a>(&'a self, executor: &'a WgpuExecutor, args: &'a Self::Args<'_>) -> Self::Out { + fn run(&self, executor: &WgpuExecutor, args: &Self::Args<'_>) -> Self::Out { let frame = super::render::Frame::new(args.strokes)?; let region = Region::new(&args.footprint)?; let state = args.cache.take(&args.footprint).unwrap_or_default(); diff --git a/node-graph/nodes/gstd/src/render_background.rs b/node-graph/nodes/gstd/src/render_background.rs index 3de95a83e9f..938a83186be 100644 --- a/node-graph/nodes/gstd/src/render_background.rs +++ b/node-graph/nodes/gstd/src/render_background.rs @@ -9,7 +9,7 @@ use graph_craft::document::value::{RenderOutput, RenderOutputType}; use graphic_types::raster_types::Texture; use rendering::{RenderParams, SvgRender, SvgRenderOutput}; use std::fmt::Write; -use wgpu_executor::{AsyncWgpuPipeline, Buffer, WgpuExecutor, WgpuPipelineCache}; +use wgpu_executor::{Buffer, WgpuExecutor, WgpuPipeline, WgpuPipelineCache}; #[node_macro::node(category(""))] async fn render_background<'a: 'n>( @@ -35,15 +35,12 @@ async fn render_background<'a: 'n>( let data = match foreground_data { RenderOutputType::Texture(foreground_texture) => { let doc_to_screen = render_params.footprint.transform.as_affine2(); - let blended = pipeline - .into_element() - .run::(&CompositeBackgroundArgs { - foreground: foreground_texture.as_ref(), - backgrounds: &metadata.backgrounds, - document_to_screen: doc_to_screen, - zoom: render_params.viewport_zoom.to_f32(), - }) - .await; + let blended = pipeline.into_element().run::(&CompositeBackgroundArgs { + foreground: foreground_texture.as_ref(), + backgrounds: &metadata.backgrounds, + document_to_screen: doc_to_screen, + zoom: render_params.viewport_zoom.to_f32(), + }); RenderOutputType::Texture(blended) } @@ -149,7 +146,7 @@ pub struct CompositeBackgroundArgs<'a> { zoom: f32, } -impl AsyncWgpuPipeline for CompositeBackground { +impl WgpuPipeline for CompositeBackground { type Args<'a> = CompositeBackgroundArgs<'a>; type Out = Texture; @@ -332,7 +329,7 @@ impl AsyncWgpuPipeline for CompositeBackground { } } - async fn run<'a>(&'a self, executor: &'a WgpuExecutor, args: &'a Self::Args<'_>) -> Self::Out { + fn run(&self, executor: &WgpuExecutor, args: &Self::Args<'_>) -> Self::Out { let &CompositeBackgroundArgs { foreground, backgrounds, diff --git a/node-graph/nodes/gstd/src/render_node.rs b/node-graph/nodes/gstd/src/render_node.rs index 58d23542844..43bff7e3686 100644 --- a/node-graph/nodes/gstd/src/render_node.rs +++ b/node-graph/nodes/gstd/src/render_node.rs @@ -136,7 +136,6 @@ async fn render<'a: 'n>( .into_element() .expect("GPU executor not available") .render_vello_scene(&transformed_scene, footprint.resolution, context, None) - .await .expect("Failed to render Vello scene"); RenderOutputType::Texture(texture) } diff --git a/node-graph/nodes/gstd/src/render_pixel_preview.rs b/node-graph/nodes/gstd/src/render_pixel_preview.rs index 427e1a42143..d7447160cc9 100644 --- a/node-graph/nodes/gstd/src/render_pixel_preview.rs +++ b/node-graph/nodes/gstd/src/render_pixel_preview.rs @@ -6,7 +6,7 @@ use graph_craft::document::value::{RenderOutput, RenderOutputType}; use graphic_types::raster_types::Texture; use rendering::{RenderOutputType as RenderOutputTypeRequest, RenderParams}; use vector_types::vector::style::RenderMode; -use wgpu_executor::{AsyncWgpuPipeline, WgpuExecutor, WgpuPipelineCache}; +use wgpu_executor::{WgpuExecutor, WgpuPipeline, WgpuPipelineCache}; #[node_macro::node(category(""))] async fn render_pixel_preview<'a: 'n>( @@ -62,14 +62,11 @@ async fn render_pixel_preview<'a: 'n>( let logical_transform = DAffine2::from_scale(DVec2::splat(1. / physical_scale)) * footprint.transform; let transform = DAffine2::from_translation(-upstream_min) * logical_transform.inverse() * DAffine2::from_scale(logical_resolution); - let resampled = pipeline - .into_element() - .run::(&PixelPreviewArgs { - source: source_texture.as_ref(), - transform: &transform, - size: physical_resolution, - }) - .await; + let resampled = pipeline.into_element().run::(&PixelPreviewArgs { + source: source_texture.as_ref(), + transform: &transform, + size: physical_resolution, + }); result.data = RenderOutputType::Texture(resampled); @@ -101,7 +98,7 @@ pub struct PixelPreviewArgs<'a> { size: UVec2, } -impl AsyncWgpuPipeline for PixelPreview { +impl WgpuPipeline for PixelPreview { type Args<'a> = PixelPreviewArgs<'a>; type Out = Texture; @@ -173,7 +170,7 @@ impl AsyncWgpuPipeline for PixelPreview { PixelPreview { pipeline, bind_group_layout } } - async fn run<'a>(&'a self, executor: &'a WgpuExecutor, args: &'a Self::Args<'_>) -> Self::Out { + fn run(&self, executor: &WgpuExecutor, args: &Self::Args<'_>) -> Self::Out { let context = &executor.context(); let &PixelPreviewArgs { source, transform, size } = args; From f09b77c30c8115fe92a9602230e9efc0b20a674d Mon Sep 17 00:00:00 2001 From: Timon Date: Tue, 29 Sep 2026 15:44:51 +0000 Subject: [PATCH 13/54] Make convert, brush and render nodes sync --- node-graph/nodes/brush/src/basic_brush/mod.rs | 2 +- node-graph/nodes/gcore/src/ops.rs | 4 ++-- node-graph/nodes/gstd/src/platform_application_io.rs | 2 +- node-graph/nodes/gstd/src/render_background.rs | 2 +- node-graph/nodes/gstd/src/render_node.rs | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/node-graph/nodes/brush/src/basic_brush/mod.rs b/node-graph/nodes/brush/src/basic_brush/mod.rs index 7b8ca11744d..0c4b7ee4757 100644 --- a/node-graph/nodes/brush/src/basic_brush/mod.rs +++ b/node-graph/nodes/brush/src/basic_brush/mod.rs @@ -15,7 +15,7 @@ use raster_types::{GPU, Raster}; use wgpu_executor::{WgpuExecutor, WgpuPipelineCache}; #[node_macro::node(category("Raster: Brush"))] -async fn basic_brush<'a: 'n>( +fn basic_brush<'a: 'n>( ctx: impl Ctx + ExtractFootprint, strokes: List, #[widget(ParsedWidgetOverride::Hidden)] cache: Item, diff --git a/node-graph/nodes/gcore/src/ops.rs b/node-graph/nodes/gcore/src/ops.rs index e8326166842..9c656565e64 100644 --- a/node-graph/nodes/gcore/src/ops.rs +++ b/node-graph/nodes/gcore/src/ops.rs @@ -92,7 +92,7 @@ fn embed_list<'i, T: 'i + Send, E: 'i + Send + From>>(_: impl Ctx, value /// The [`Convert`]-based counterpart of `into_item`, casting an `Item` wire's element to a connector's numeric element type. #[node_macro::node(category(""), skip_impl)] -async fn convert_item<'i, T: 'i + Send + Convert, E: 'i + Send>(ctx: impl Ctx + ExtractFootprint, value: Item, _element_ty: PhantomData) -> Item { +fn convert_item<'i, T: 'i + Send + Convert, E: 'i + Send>(ctx: impl Ctx + ExtractFootprint, value: Item, _element_ty: PhantomData) -> Item { let footprint = *ctx.try_footprint().unwrap_or(&Footprint::DEFAULT); let (value, attributes) = value.into_parts(); @@ -101,7 +101,7 @@ async fn convert_item<'i, T: 'i + Send + Convert, E: 'i + Send>(ctx: impl /// The `List` counterpart of `convert_item`, casting every element to the connector's numeric element type. #[node_macro::node(category(""), skip_impl)] -async fn convert_list<'i, T: 'i + Send + Convert, E: 'i + Send>(ctx: impl Ctx + ExtractFootprint, value: List, _element_ty: PhantomData) -> List { +fn convert_list<'i, T: 'i + Send + Convert, E: 'i + Send>(ctx: impl Ctx + ExtractFootprint, value: List, _element_ty: PhantomData) -> List { let footprint = *ctx.try_footprint().unwrap_or(&Footprint::DEFAULT); let mut result = List::default(); diff --git a/node-graph/nodes/gstd/src/platform_application_io.rs b/node-graph/nodes/gstd/src/platform_application_io.rs index 7db1bf5b61d..5b6e70e4d0d 100644 --- a/node-graph/nodes/gstd/src/platform_application_io.rs +++ b/node-graph/nodes/gstd/src/platform_application_io.rs @@ -288,7 +288,7 @@ fn try_wgpu_executor<'a: 'n>(_: impl Ctx, #[scope(editor_api::IDENTIFIER)] edito /// Uploads image data from CPU memory into a GPU texture so that GPU-based nodes can process it. #[node_macro::node(category("Debug"), memoize)] -async fn upload_texture<'a: 'n>(_: impl Ctx, content: Item>, #[scope(wgpu_executor::IDENTIFIER)] executor: Item<&'a ::wgpu_executor::WgpuExecutor>) -> Item> { +fn upload_texture<'a: 'n>(_: impl Ctx, content: Item>, #[scope(wgpu_executor::IDENTIFIER)] executor: Item<&'a ::wgpu_executor::WgpuExecutor>) -> Item> { let executor = executor.into_element(); let (raster, attributes) = content.into_parts(); diff --git a/node-graph/nodes/gstd/src/render_background.rs b/node-graph/nodes/gstd/src/render_background.rs index 938a83186be..f7bd70320a1 100644 --- a/node-graph/nodes/gstd/src/render_background.rs +++ b/node-graph/nodes/gstd/src/render_background.rs @@ -12,7 +12,7 @@ use std::fmt::Write; use wgpu_executor::{Buffer, WgpuExecutor, WgpuPipeline, WgpuPipelineCache}; #[node_macro::node(category(""))] -async fn render_background<'a: 'n>( +fn render_background<'a: 'n>( ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, #[scope(composite_background_pipeline::IDENTIFIER)] pipeline: Item, data: Item, diff --git a/node-graph/nodes/gstd/src/render_node.rs b/node-graph/nodes/gstd/src/render_node.rs index 43bff7e3686..65e8f4f45f8 100644 --- a/node-graph/nodes/gstd/src/render_node.rs +++ b/node-graph/nodes/gstd/src/render_node.rs @@ -76,7 +76,7 @@ async fn render_intermediate<'a: 'n, T: 'static + Render + WasmNotSend + Send + } #[node_macro::node(category(""))] -async fn render<'a: 'n>( +fn render<'a: 'n>( ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, #[scope(crate::platform_application_io::try_wgpu_executor::IDENTIFIER)] executor: Item>, data: Item, From dea878acf4a657ef06759db5a020f821e0409193 Mon Sep 17 00:00:00 2001 From: Timon Date: Tue, 29 Sep 2026 22:51:22 +0000 Subject: [PATCH 14/54] Pass editor API as owned Arc --- node-graph/graph-craft/src/document/value.rs | 24 +++---------------- .../src/dynamic_executor.rs | 13 +++------- node-graph/interpreted-executor/src/util.rs | 2 +- 3 files changed, 7 insertions(+), 32 deletions(-) diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 8cf346d1dad..9dbc08b4db7 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -22,7 +22,6 @@ use graphic_types::{Artboard, Graphic, Vector}; use rendering::RenderMetadata; use std::fmt::Display; use std::hash::Hash; -use std::marker::PhantomData; use std::str::FromStr; pub use std::sync::Arc; use text_nodes::Font; @@ -222,7 +221,7 @@ macro_rules! tagged_value { Self::NodeIdPath(path) => Box::new(Item::new_from_element(path)), Self::DocumentNode(node) => Box::new(node), Self::ContextFeatures(features) => Box::new(Item::new_from_element(features)), - Self::EditorApi(x) => Box::new(x), + Self::EditorApi(x) => Box::new(Item::new_from_element(x)), Self::ResourceHash(x) => Box::new(Item::new_from_element(x)), } } @@ -290,7 +289,7 @@ macro_rules! tagged_value { Self::NodeIdPath(path) => Arc::new(Item::new_from_element(path)), Self::DocumentNode(node) => Arc::new(node), Self::ContextFeatures(features) => Arc::new(Item::new_from_element(features)), - Self::EditorApi(x) => Arc::new(x), + Self::EditorApi(x) => Arc::new(Item::new_from_element(x)), Self::ResourceHash(x) => Arc::new(Item::new_from_element(x)), } } @@ -321,7 +320,7 @@ macro_rules! tagged_value { Self::NodeIdPath(_) => item!(NodeIdPath), Self::DocumentNode(_) => concrete!(DocumentNode), Self::ContextFeatures(_) => item!(ContextFeatures), - Self::EditorApi(_) => item!(&PlatformEditorApi), + Self::EditorApi(_) => item!(Arc), Self::ResourceHash(_) => item!(ResourceHash), }; @@ -878,23 +877,6 @@ impl UpcastNode { Self { value } } } -#[derive(Default, Debug, Clone, Copy)] -pub struct UpcastAsRefNode + Sync + Send, U: Sync + Send>(pub T, PhantomData); - -impl<'i, T: 'i + AsRef + Sync + Send, U: 'i + StaticType + Sync + Send> Node<'i, DAny<'i>> for UpcastAsRefNode { - type Output = FutureAny<'i>; - #[inline(always)] - fn eval(&'i self, _: DAny<'i>) -> Self::Output { - Box::pin(async move { Box::new(Item::new_from_element(self.0.as_ref())) as DAny<'i> }) - } -} - -impl + Sync + Send, U: Sync + Send> UpcastAsRefNode { - pub const fn new(value: T) -> UpcastAsRefNode { - UpcastAsRefNode(value, PhantomData) - } -} - #[derive(Debug, Clone, PartialEq, dyn_any::DynAny, serde::Serialize, serde::Deserialize)] pub struct RenderOutput { pub data: RenderOutputType, diff --git a/node-graph/interpreted-executor/src/dynamic_executor.rs b/node-graph/interpreted-executor/src/dynamic_executor.rs index eda5a871a51..5b9205a886a 100644 --- a/node-graph/interpreted-executor/src/dynamic_executor.rs +++ b/node-graph/interpreted-executor/src/dynamic_executor.rs @@ -2,7 +2,7 @@ use crate::node_registry; use dyn_any::StaticType; use graph_craft::Type; use graph_craft::document::NodeId; -use graph_craft::document::value::{TaggedValue, UpcastAsRefNode, UpcastNode}; +use graph_craft::document::value::{TaggedValue, UpcastNode}; use graph_craft::graphene_compiler::Executor; use graph_craft::proto::{ConstructionArgs, GraphError, LocalFuture, NodeContainer, ProtoNetwork, ProtoNode, SharedNodeContainer, TypeErasedBox, TypingContext}; use graph_craft::proto::{GraphErrorType, GraphErrors}; @@ -395,15 +395,8 @@ impl BorrowTree { match &proto_node.construction_args { ConstructionArgs::Value(value) => { - let node = if let TaggedValue::EditorApi(api) = &**value { - let editor_api = UpcastAsRefNode::new(api.clone()); - let node = Box::new(editor_api) as TypeErasedBox<'_>; - NodeContainer::new(node) - } else { - let upcasted = UpcastNode::new(value.to_owned()); - let node = Box::new(upcasted) as TypeErasedBox<'_>; - NodeContainer::new(node) - }; + let upcasted = UpcastNode::new(value.to_owned()); + let node = NodeContainer::new(Box::new(upcasted) as TypeErasedBox<'_>); self.store_node(node, id, path.into()); } ConstructionArgs::Inline(_) => unimplemented!("Inline nodes are not supported yet"), diff --git a/node-graph/interpreted-executor/src/util.rs b/node-graph/interpreted-executor/src/util.rs index 1c48c990cf2..612cdaba5ce 100644 --- a/node-graph/interpreted-executor/src/util.rs +++ b/node-graph/interpreted-executor/src/util.rs @@ -114,7 +114,7 @@ pub fn wrap_network_in_scope(network: NodeNetwork, editor_api: Arc)))]; NodeNetwork { exports: vec![NodeInput::node(NodeId(1), 0)], From 28f224628868b08f4a09b4812bd49a09128895db Mon Sep 17 00:00:00 2001 From: Timon Date: Tue, 29 Sep 2026 22:51:22 +0000 Subject: [PATCH 15/54] Take scope inputs by value --- .../interpreted-executor/src/node_registry.rs | 15 ++++++------ .../wgpu-executor/src/texture_conversion.rs | 20 ++++++++-------- .../src/shader_nodes/per_pixel_adjust.rs | 4 ++-- node-graph/nodes/brush/src/basic_brush/mod.rs | 6 ++--- .../nodes/gstd/src/platform_application_io.rs | 23 +++++++++++-------- .../nodes/gstd/src/render_background.rs | 6 ++--- node-graph/nodes/gstd/src/render_cache.rs | 8 +++---- node-graph/nodes/gstd/src/render_node.rs | 8 +++---- .../nodes/gstd/src/render_pixel_preview.rs | 6 ++--- node-graph/nodes/raster/src/std_nodes.rs | 2 +- 10 files changed, 51 insertions(+), 47 deletions(-) diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index 54677797b53..e4ad4927d4a 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -30,6 +30,7 @@ use graphene_std::vector::{QRCodeErrorCorrectionLevel, Vector, VectorModificatio use graphene_std::{Artboard, Context, Graphic, NodeIO, NodeIOTypes, ProtoNodeIdentifier, concrete, fn_type_fut, future}; use node_registry_macros::async_node; use std::collections::HashMap; +use std::sync::Arc; // TODO: turn into hashmap fn node_registry() -> HashMap> { @@ -80,16 +81,16 @@ fn node_registry() -> HashMap, input: Context, fn_params: [Context => List]), // Context nullification #[cfg(feature = "gpu")] - async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item<&PlatformEditorApi>, Context => Item]), + async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item>, Context => Item]), async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item, Context => Item]), async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item, Context => Item]), async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item, Context => Item]), async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => ListDyn, Context => Item]), #[cfg(target_family = "wasm")] async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item, Context => Item]), - async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item<&PlatformEditorApi>, Context => Item]), - async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item<&wgpu_executor::WgpuExecutor>, Context => Item]), - async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item>, Context => Item]), + async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item>, Context => Item]), + async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item, Context => Item]), + async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item>, Context => Item]), async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item, Context => Item]), // ========== // MEMO NODES @@ -135,13 +136,13 @@ fn node_registry() -> HashMap, input: Context, fn_params: [Context => Item]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<&PlatformEditorApi>]), + async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item>]), #[cfg(feature = "gpu")] async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List>]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<&wgpu_executor::WgpuExecutor>]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item>]), + async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item]), + async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item>]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item]), ]; // The per-connector input adapter, registered per element type: an `Item` or `List` wire passes through unchanged. diff --git a/node-graph/libraries/wgpu-executor/src/texture_conversion.rs b/node-graph/libraries/wgpu-executor/src/texture_conversion.rs index 8a5bac537d4..d94372bd51e 100644 --- a/node-graph/libraries/wgpu-executor/src/texture_conversion.rs +++ b/node-graph/libraries/wgpu-executor/src/texture_conversion.rs @@ -133,21 +133,21 @@ impl RasterGpuToRasterCpuConverter { } /// Passthrough conversion for GPU `List`s - no conversion needed -impl<'i> Convert>, &'i WgpuExecutor> for List> { - fn convert(self, _: Footprint, _converter: &'i WgpuExecutor) -> List> { +impl Convert>, WgpuExecutor> for List> { + fn convert(self, _: Footprint, _converter: WgpuExecutor) -> List> { self } } /// Converts a `List>` to `List>` by uploading each image to a texture -impl<'i> Convert>, &'i WgpuExecutor> for List> { - fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> List> { +impl Convert>, WgpuExecutor> for List> { + fn convert(self, _: Footprint, executor: WgpuExecutor) -> List> { let queue = executor.context().queue.lock(); let list = self .into_iter() .map(|row| { let (image, attributes) = row.into_parts(); - let texture = upload_to_texture(executor, &queue, &image); + let texture = upload_to_texture(&executor, &queue, &image); Item::from_parts(Raster::new_gpu(texture), attributes) }) @@ -159,10 +159,10 @@ impl<'i> Convert>, &'i WgpuExecutor> for List> { } /// Converts single CPU raster to GPU by uploading to texture -impl<'i> Convert, &'i WgpuExecutor> for Raster { - fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> Raster { +impl Convert, WgpuExecutor> for Raster { + fn convert(self, _: Footprint, executor: WgpuExecutor) -> Raster { let queue = executor.context().queue.lock(); - let texture = upload_to_texture(executor, &queue, &self); + let texture = upload_to_texture(&executor, &queue, &self); queue.submit([]); Raster::new_gpu(texture) @@ -170,8 +170,8 @@ impl<'i> Convert, &'i WgpuExecutor> for Raster { } /// Passthrough conversion for CPU `List`s - no conversion needed -impl<'i> Convert>, &'i WgpuExecutor> for List> { - fn convert(self, _: Footprint, _converter: &'i WgpuExecutor) -> List> { +impl Convert>, WgpuExecutor> for List> { + fn convert(self, _: Footprint, _converter: WgpuExecutor) -> List> { self } } 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 25369a5e323..1d3372ad030 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 @@ -255,7 +255,7 @@ impl PerPixelAdjustCodegen<'_> { description: "".to_string(), widget_override: Default::default(), ty: ParsedFieldType::classify(RegularParsedField { - ty: parse_quote!(#gcore::list::Item<&'a WgpuExecutor>), + ty: parse_quote!(#gcore::list::Item), exposed: true, value_source: ParsedValueSource::Scope(Box::new(parse_quote!("graphene_std::platform_application_io::WgpuExecutorNode"))), number_soft_min: None, @@ -343,7 +343,7 @@ impl PerPixelAdjustCodegen<'_> { fn_name: self.shader_node_mod.clone(), struct_name: format_ident!("{}", self.shader_node_mod.to_string().to_case(Case::Pascal)), mod_name: self.shader_node_mod.clone(), - fn_generics: vec![parse_quote!('a: 'n)], + fn_generics: Vec::new(), where_clause: None, input: Input { pat_ident: self.parsed.input.pat_ident.clone(), diff --git a/node-graph/nodes/brush/src/basic_brush/mod.rs b/node-graph/nodes/brush/src/basic_brush/mod.rs index 0c4b7ee4757..b0515c424e6 100644 --- a/node-graph/nodes/brush/src/basic_brush/mod.rs +++ b/node-graph/nodes/brush/src/basic_brush/mod.rs @@ -15,7 +15,7 @@ use raster_types::{GPU, Raster}; use wgpu_executor::{WgpuExecutor, WgpuPipelineCache}; #[node_macro::node(category("Raster: Brush"))] -fn basic_brush<'a: 'n>( +fn basic_brush( ctx: impl Ctx + ExtractFootprint, strokes: List, #[widget(ParsedWidgetOverride::Hidden)] cache: Item, @@ -65,9 +65,9 @@ fn basic_brush<'a: 'n>( } #[node_macro::node(category(""), inject_scope)] -fn basic_brush_pipeline<'a: 'n>( +fn basic_brush_pipeline( _ctx: impl Ctx, - #[scope(ProtoNodeIdentifier::new("graphene_std::platform_application_io::WgpuExecutorNode"))] executor: Item<&'a WgpuExecutor>, + #[scope(ProtoNodeIdentifier::new("graphene_std::platform_application_io::WgpuExecutorNode"))] executor: Item, #[data] pipeline: WgpuPipelineCache, ) -> Item { executor.into_element().pipeline_init::(pipeline); diff --git a/node-graph/nodes/gstd/src/platform_application_io.rs b/node-graph/nodes/gstd/src/platform_application_io.rs index 5b6e70e4d0d..6bcaa7af841 100644 --- a/node-graph/nodes/gstd/src/platform_application_io.rs +++ b/node-graph/nodes/gstd/src/platform_application_io.rs @@ -24,6 +24,7 @@ use graphic_types::raster_types::Image; use graphic_types::raster_types::{CPU, GPU, Raster}; #[cfg(target_family = "wasm")] use rendering::{Render, RenderParams, RenderSvgSegmentList, SvgRender}; +use std::sync::Arc; fn parse_headers(headers: &str) -> reqwest::header::HeaderMap { use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; @@ -139,7 +140,7 @@ fn image_to_bytes(_: impl Ctx, image: Item>) -> Item { /// Loads binary from URLs and local asset paths. Returns a transparent placeholder if the resource fails to load, allowing rendering to continue. #[node_macro::node(category("Web Request"))] -async fn load_resource<'a: 'n>(_: impl Ctx, _primary: (), #[name("URL")] url: Item) -> Item { +async fn load_resource(_: impl Ctx, _primary: (), #[name("URL")] url: Item) -> Item { let url = url.into_element(); let placeholder = || -> Item { Item::new_from_element(Resource::empty()) }; @@ -239,23 +240,24 @@ async fn rasterize(_: impl Ctx, data: List, footprint: Item, } #[node_macro::node(category(""), inject_scope)] -fn editor_api<'a: 'n>(_: impl Ctx, #[scope("editor-api")] editor_api: Item<&'a PlatformEditorApi>) -> Item<&'a PlatformEditorApi> { +fn editor_api(_: impl Ctx, #[scope("editor-api")] editor_api: Item>) -> Item> { editor_api } #[node_macro::node(category(""))] -async fn resource<'a: 'n>( +async fn resource( _: impl Ctx, /// The scope-provided editor API giving access to the platform's resource storage. #[scope(editor_api::IDENTIFIER)] - editor_api: Item<&'a PlatformEditorApi>, + editor_api: Item>, /// The content hash identifying which stored resource to load. hash: Item, ) -> Item { let hash = hash.into_element(); let placeholder = || -> Item { Item::new_from_element(Resource::empty()) }; - let Some(application_io) = editor_api.into_element().application_io.as_ref() else { + let editor_api = editor_api.into_element(); + let Some(application_io) = editor_api.application_io.as_ref() else { log::error!("Resource {hash} is unavailable because the platform's application IO is missing"); return placeholder(); }; @@ -269,26 +271,27 @@ async fn resource<'a: 'n>( } #[node_macro::node(category(""), inject_scope)] -fn wgpu_executor<'a: 'n>(_: impl Ctx, #[scope(editor_api::IDENTIFIER)] editor_api: Item<&'a PlatformEditorApi>) -> Item<&'a ::wgpu_executor::WgpuExecutor> { +fn wgpu_executor(_: impl Ctx, #[scope(editor_api::IDENTIFIER)] editor_api: Item>) -> Item<::wgpu_executor::WgpuExecutor> { let executor = editor_api .into_element() .application_io .as_ref() .expect("ApplicationIo not available") .gpu_executor() - .expect("GPU executor not available"); + .expect("GPU executor not available") + .clone(); Item::new_from_element(executor) } #[node_macro::node(category(""), inject_scope)] -fn try_wgpu_executor<'a: 'n>(_: impl Ctx, #[scope(editor_api::IDENTIFIER)] editor_api: Item<&'a PlatformEditorApi>) -> Item> { - let executor = editor_api.into_element().application_io.as_ref().and_then(|application_io| application_io.gpu_executor()); +fn try_wgpu_executor(_: impl Ctx, #[scope(editor_api::IDENTIFIER)] editor_api: Item>) -> Item> { + let executor = editor_api.into_element().application_io.as_ref().and_then(|application_io| application_io.gpu_executor()).cloned(); Item::new_from_element(executor) } /// Uploads image data from CPU memory into a GPU texture so that GPU-based nodes can process it. #[node_macro::node(category("Debug"), memoize)] -fn upload_texture<'a: 'n>(_: impl Ctx, content: Item>, #[scope(wgpu_executor::IDENTIFIER)] executor: Item<&'a ::wgpu_executor::WgpuExecutor>) -> Item> { +fn upload_texture(_: impl Ctx, content: Item>, #[scope(wgpu_executor::IDENTIFIER)] executor: Item<::wgpu_executor::WgpuExecutor>) -> Item> { let executor = executor.into_element(); let (raster, attributes) = content.into_parts(); diff --git a/node-graph/nodes/gstd/src/render_background.rs b/node-graph/nodes/gstd/src/render_background.rs index f7bd70320a1..94e69ea81d2 100644 --- a/node-graph/nodes/gstd/src/render_background.rs +++ b/node-graph/nodes/gstd/src/render_background.rs @@ -12,7 +12,7 @@ use std::fmt::Write; use wgpu_executor::{Buffer, WgpuExecutor, WgpuPipeline, WgpuPipelineCache}; #[node_macro::node(category(""))] -fn render_background<'a: 'n>( +fn render_background( ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, #[scope(composite_background_pipeline::IDENTIFIER)] pipeline: Item, data: Item, @@ -119,9 +119,9 @@ fn render_background<'a: 'n>( } #[node_macro::node(category(""), inject_scope)] -fn composite_background_pipeline<'a: 'n>( +fn composite_background_pipeline( _ctx: impl Ctx, - #[scope(crate::platform_application_io::try_wgpu_executor::IDENTIFIER)] executor: Item>, + #[scope(crate::platform_application_io::try_wgpu_executor::IDENTIFIER)] executor: Item>, #[data] pipeline: WgpuPipelineCache, ) -> Item { if let Some(executor) = executor.into_element() { diff --git a/node-graph/nodes/gstd/src/render_cache.rs b/node-graph/nodes/gstd/src/render_cache.rs index 683bac50ad3..cccc3319eec 100644 --- a/node-graph/nodes/gstd/src/render_cache.rs +++ b/node-graph/nodes/gstd/src/render_cache.rs @@ -322,10 +322,10 @@ fn flood_fill(start: &TileCoord, tile_set: &HashSet, visited: &mut Ha } #[node_macro::node(category(""))] -async fn render_output_cache<'a: 'n>( +async fn render_output_cache( ctx: impl Ctx + ExtractAll + CloneVarArgs + ExtractRealTime + ExtractAnimationTime + ExtractPointerPosition + Sync, - #[scope(crate::platform_application_io::try_wgpu_executor::IDENTIFIER)] executor: Item>, - #[scope(crate::platform_application_io::editor_api::IDENTIFIER)] editor_api: Item<&'a PlatformEditorApi>, + #[scope(crate::platform_application_io::try_wgpu_executor::IDENTIFIER)] executor: Item>, + #[scope(crate::platform_application_io::editor_api::IDENTIFIER)] editor_api: Item>, data: impl Node, Output = Item> + Send + Sync, #[data] tile_cache: TileCache, ) -> Item { @@ -393,7 +393,7 @@ async fn render_output_cache<'a: 'n>( let executor = executor.into_element().expect("GPU executor not available"); let output_texture = executor.request_texture(physical_resolution); - let combined_metadata = composite_cached_regions(&all_regions, &output_texture, &device_origin_offset, &footprint.transform, executor); + let combined_metadata = composite_cached_regions(&all_regions, &output_texture, &device_origin_offset, &footprint.transform, &executor); Item::new_from_element(RenderOutput { data: RenderOutputType::Texture(output_texture), diff --git a/node-graph/nodes/gstd/src/render_node.rs b/node-graph/nodes/gstd/src/render_node.rs index 65e8f4f45f8..54c4f1f6309 100644 --- a/node-graph/nodes/gstd/src/render_node.rs +++ b/node-graph/nodes/gstd/src/render_node.rs @@ -23,7 +23,7 @@ pub struct RenderIntermediate { } #[node_macro::node(category(""))] -async fn render_intermediate<'a: 'n, T: 'static + Render + WasmNotSend + Send + Sync>( +async fn render_intermediate( ctx: impl Ctx + ExtractVarArgs + ExtractAll + CloneVarArgs, #[implementations( Context -> List, @@ -76,9 +76,9 @@ async fn render_intermediate<'a: 'n, T: 'static + Render + WasmNotSend + Send + } #[node_macro::node(category(""))] -fn render<'a: 'n>( +fn render( ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, - #[scope(crate::platform_application_io::try_wgpu_executor::IDENTIFIER)] executor: Item>, + #[scope(crate::platform_application_io::try_wgpu_executor::IDENTIFIER)] executor: Item>, data: Item, ) -> Item { let footprint = ctx.footprint(); @@ -146,7 +146,7 @@ fn render<'a: 'n>( } #[node_macro::node(category(""))] -async fn create_context<'a: 'n>( +async fn create_context( // The executor boundary supplies the render config as the sole vararg (see `wrap_network_in_scope()`) ctx: impl Ctx + ExtractAll + CloneVarArgs + Sync, data: impl Node, Output = Item>, diff --git a/node-graph/nodes/gstd/src/render_pixel_preview.rs b/node-graph/nodes/gstd/src/render_pixel_preview.rs index d7447160cc9..14bd50ed536 100644 --- a/node-graph/nodes/gstd/src/render_pixel_preview.rs +++ b/node-graph/nodes/gstd/src/render_pixel_preview.rs @@ -9,7 +9,7 @@ use vector_types::vector::style::RenderMode; use wgpu_executor::{WgpuExecutor, WgpuPipeline, WgpuPipelineCache}; #[node_macro::node(category(""))] -async fn render_pixel_preview<'a: 'n>( +async fn render_pixel_preview( ctx: impl Ctx + ExtractAll + CloneVarArgs + Sync, #[scope(pixel_preview_pipeline::IDENTIFIER)] pipeline: Item, data: impl Node, Output = Item> + Send + Sync, @@ -76,9 +76,9 @@ async fn render_pixel_preview<'a: 'n>( } #[node_macro::node(category(""), inject_scope)] -fn pixel_preview_pipeline<'a: 'n>( +fn pixel_preview_pipeline( _ctx: impl Ctx, - #[scope(crate::platform_application_io::try_wgpu_executor::IDENTIFIER)] executor: Item>, + #[scope(crate::platform_application_io::try_wgpu_executor::IDENTIFIER)] executor: Item>, #[data] pipeline: WgpuPipelineCache, ) -> Item { if let Some(executor) = executor.into_element() { diff --git a/node-graph/nodes/raster/src/std_nodes.rs b/node-graph/nodes/raster/src/std_nodes.rs index 9cb7168cb13..ba772e3b85b 100644 --- a/node-graph/nodes/raster/src/std_nodes.rs +++ b/node-graph/nodes/raster/src/std_nodes.rs @@ -254,7 +254,7 @@ fn empty_image(_: impl Ctx, transform: Item, color: Item) -> It /// /// Reads PNG, JPG, GIF, WEBP, TIFF, BMP, TGA, ICO, HDR, and EXR files. Light brighter than white, as HDR and EXR files can hold, is clipped to white. #[node_macro::node(category("Raster"))] -fn image<'a: 'n>( +fn image( _: impl Ctx, _primary: (), /// The image file to display. From bc6e52b9f6a9ff651c13d619a38e86c5429aae95 Mon Sep 17 00:00:00 2001 From: Timon Date: Wed, 30 Sep 2026 01:40:00 +0000 Subject: [PATCH 16/54] Move Spawn and Task into core-types --- editor/src/task.rs | 41 ++++++++----------- node-graph/libraries/core-types/src/lib.rs | 1 + .../libraries/core-types/src/runtime.rs | 10 +++++ 3 files changed, 27 insertions(+), 25 deletions(-) create mode 100644 node-graph/libraries/core-types/src/runtime.rs diff --git a/editor/src/task.rs b/editor/src/task.rs index d37d5d52fff..802b5c4c559 100644 --- a/editor/src/task.rs +++ b/editor/src/task.rs @@ -1,50 +1,41 @@ -use std::pin::Pin; +pub use graphene_std::runtime::{Spawn, Task}; use std::sync::Arc; -#[cfg(not(target_family = "wasm"))] -pub type Task = Pin + Send + 'static>>; -#[cfg(target_family = "wasm")] -pub type Task = Pin + 'static>>; - -pub trait Spawn { - fn spawn(&self, task: Task); -} - #[derive(Clone)] pub struct TaskExecutorHandle { - spawner: Arc, + backend: Arc, } impl Spawn for TaskExecutorHandle { fn spawn(&self, task: Task) { - self.spawner.spawn(task); + self.backend.spawn(task); } } pub struct TaskExecutor { - spawner: Arc, + backend: Arc, } impl TaskExecutor { pub fn new() -> Self { - let spawner = { + let backend = { #[cfg(target_family = "wasm")] { - Arc::new(Spawner::Wasm(WasmSpawner)) + Arc::new(Backend::Wasm(WasmSpawner)) } #[cfg(not(target_family = "wasm"))] { - Arc::new(Spawner::Tokio(TokioSpawner::new())) + Arc::new(Backend::Tokio(TokioSpawner::new())) } }; - Self { spawner } + Self { backend } } pub fn blocking() -> Self { - let spawner = Arc::new(Spawner::Blocking(BlockingSpawner)); - Self { spawner } + let backend = Arc::new(Backend::Blocking(BlockingSpawner)); + Self { backend } } pub fn handle(&self) -> TaskExecutorHandle { - TaskExecutorHandle { spawner: self.spawner.clone() } + TaskExecutorHandle { backend: self.backend.clone() } } } @@ -54,7 +45,7 @@ impl Default for TaskExecutor { } } -enum Spawner { +enum Backend { #[cfg(target_family = "wasm")] Wasm(WasmSpawner), #[cfg(not(target_family = "wasm"))] @@ -62,14 +53,14 @@ enum Spawner { Blocking(BlockingSpawner), } -impl Spawn for Spawner { +impl Spawn for Backend { fn spawn(&self, task: Task) { match self { #[cfg(target_family = "wasm")] - Spawner::Wasm(spawner) => spawner.spawn(task), + Backend::Wasm(spawner) => spawner.spawn(task), #[cfg(not(target_family = "wasm"))] - Spawner::Tokio(spawner) => spawner.spawn(task), - Spawner::Blocking(spawner) => spawner.spawn(task), + Backend::Tokio(spawner) => spawner.spawn(task), + Backend::Blocking(spawner) => spawner.spawn(task), } } } diff --git a/node-graph/libraries/core-types/src/lib.rs b/node-graph/libraries/core-types/src/lib.rs index 73b02bb8a77..eeede43a0a6 100644 --- a/node-graph/libraries/core-types/src/lib.rs +++ b/node-graph/libraries/core-types/src/lib.rs @@ -13,6 +13,7 @@ pub mod none; pub mod ops; pub mod registry; pub mod render_complexity; +pub mod runtime; pub mod transfer_curve; pub mod transform; pub mod uuid; diff --git a/node-graph/libraries/core-types/src/runtime.rs b/node-graph/libraries/core-types/src/runtime.rs new file mode 100644 index 00000000000..b5e34ab0adb --- /dev/null +++ b/node-graph/libraries/core-types/src/runtime.rs @@ -0,0 +1,10 @@ +use std::pin::Pin; + +#[cfg(not(target_family = "wasm"))] +pub type Task = Pin + Send + 'static>>; +#[cfg(target_family = "wasm")] +pub type Task = Pin + 'static>>; + +pub trait Spawn { + fn spawn(&self, task: Task); +} From bf51294b77f745c8c183d98002f2f5d18ece3216 Mon Sep 17 00:00:00 2001 From: Timon Date: Wed, 30 Sep 2026 02:04:48 +0000 Subject: [PATCH 17/54] Add Poll type and status tracking --- node-graph/libraries/core-types/src/lib.rs | 1 + node-graph/libraries/core-types/src/poll.rs | 295 ++++++++++++++++++++ 2 files changed, 296 insertions(+) create mode 100644 node-graph/libraries/core-types/src/poll.rs diff --git a/node-graph/libraries/core-types/src/lib.rs b/node-graph/libraries/core-types/src/lib.rs index eeede43a0a6..e6d76586aec 100644 --- a/node-graph/libraries/core-types/src/lib.rs +++ b/node-graph/libraries/core-types/src/lib.rs @@ -11,6 +11,7 @@ pub mod memo; pub mod misc; pub mod none; pub mod ops; +pub mod poll; pub mod registry; pub mod render_complexity; pub mod runtime; diff --git a/node-graph/libraries/core-types/src/poll.rs b/node-graph/libraries/core-types/src/poll.rs new file mode 100644 index 00000000000..82edf0dd0f1 --- /dev/null +++ b/node-graph/libraries/core-types/src/poll.rs @@ -0,0 +1,295 @@ +use std::cell::Cell; +use std::error::Error; +use std::sync::Arc; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Poll { + Pending, + Final(T), + Partial(T), + Fallback(Box<(T, NodeError)>), + Error(Box), +} + +impl Poll { + pub fn fallback(value: T, error: impl Into>) -> Self { + Poll::Fallback(Box::new((value, NodeError::new(error)))) + } + + pub fn error(error: impl Into>) -> Self { + Poll::Error(Box::new(NodeError::new(error))) + } + + #[inline(always)] + pub fn map(self, f: impl FnOnce(T) -> U) -> Poll { + match self { + Poll::Pending => Poll::Pending, + Poll::Final(value) => Poll::Final(f(value)), + Poll::Partial(value) => Poll::Partial(f(value)), + Poll::Fallback(boxed) => { + let (value, error) = *boxed; + Poll::Fallback(Box::new((f(value), error))) + } + Poll::Error(error) => Poll::Error(error), + } + } + + pub fn value(self) -> Option { + match self { + Poll::Final(value) | Poll::Partial(value) => Some(value), + Poll::Fallback(boxed) => Some(boxed.0), + Poll::Pending | Poll::Error(_) => None, + } + } +} + +impl From for Poll { + fn from(interrupt: Interrupt) -> Self { + match interrupt { + Interrupt::Pending => Poll::Pending, + Interrupt::Error(error) => Poll::Error(error), + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Interrupt { + Pending, + Error(Box), +} + +#[derive(Clone, Debug)] +pub struct NodeError { + error: Arc, + trace: Vec, +} + +impl NodeError { + pub fn new(error: impl Into>) -> Self { + Self { + error: Arc::from(error.into()), + trace: Vec::new(), + } + } + + pub fn trace(&self) -> &[usize] { + &self.trace + } +} + +impl PartialEq for NodeError { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.error, &other.error) && self.trace == other.trace + } +} + +impl Eq for NodeError {} + +impl std::fmt::Display for NodeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.error.fmt(f) + } +} + +impl Error for NodeError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + self.error.source() + } +} + +impl From for Interrupt { + fn from(error: NodeError) -> Self { + Interrupt::Error(Box::new(error)) + } +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum Finality { + #[default] + Final, + Partial, +} + +#[derive(Default)] +pub struct Status { + finality: Cell, + error: Cell>, + interrupt: Cell>, + no_partial: bool, +} + +impl Status { + pub fn new() -> Self { + Self::default() + } + + pub fn no_partial() -> Self { + Self { no_partial: true, ..Self::default() } + } + + pub fn finality(&self) -> Finality { + self.finality.get() + } + + #[inline(always)] + pub fn record(&self, index: usize, state: Poll) -> Result { + match state { + Poll::Final(value) => Ok(value), + Poll::Partial(_) if self.no_partial => Err(self.interrupted(Interrupt::Pending)), + Poll::Partial(value) => { + self.finality.set(Finality::Partial); + Ok(value) + } + Poll::Fallback(boxed) => { + let (value, mut error) = *boxed; + error.trace.push(index); + self.fail(error); + Ok(value) + } + Poll::Pending => Err(self.interrupted(Interrupt::Pending)), + Poll::Error(mut error) => { + error.trace.push(index); + Err(self.interrupted(Interrupt::Error(error))) + } + } + } + + pub fn fail(&self, error: NodeError) { + let first = self.error.take(); + self.error.set(first.or(Some(error))); + } + + #[inline(always)] + pub fn run(self, kernel: impl FnOnce(&Self) -> Result) -> Poll { + match kernel(&self) { + Ok(value) => self.finish(value), + Err(interrupt) => self.bail(interrupt), + } + } + + #[inline(always)] + pub fn finish(self, value: T) -> Poll { + match (self.finality.get(), self.error.take()) { + (Finality::Partial, _) => Poll::Partial(value), + (Finality::Final, Some(error)) => Poll::Fallback(Box::new((value, error))), + (Finality::Final, None) => Poll::Final(value), + } + } + + #[inline(always)] + pub fn merge(self, poll: Poll) -> Poll { + match poll { + Poll::Final(value) => self.finish(value), + Poll::Partial(_) if self.no_partial => self.bail(Interrupt::Pending), + Poll::Partial(value) => { + self.finality.set(Finality::Partial); + self.finish(value) + } + Poll::Fallback(boxed) => { + let (value, error) = *boxed; + self.fail(error); + self.finish(value) + } + Poll::Pending => self.bail(Interrupt::Pending), + Poll::Error(error) => self.bail(Interrupt::Error(error)), + } + } + + #[inline(always)] + pub fn bail(self, interrupt: Interrupt) -> Poll { + match (self.interrupt.take(), interrupt) { + (Some(Interrupt::Error(error)), _) => Poll::Error(error), + (_, Interrupt::Error(_)) if self.finality.get() == Finality::Partial => Poll::Pending, + (_, interrupt) => interrupt.into(), + } + } + + fn interrupted(&self, interrupt: Interrupt) -> Interrupt { + let strongest = match (self.interrupt.take(), &interrupt) { + (Some(Interrupt::Error(error)), _) => Interrupt::Error(error), + _ => interrupt.clone(), + }; + self.interrupt.set(Some(strongest)); + interrupt + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::Arc; + + fn add(a: Poll, b: Poll) -> Poll { + Status::new().run(|cell| { + let a = cell.record(0, a); + let b = cell.record(1, b); + Ok(a? + b?) + }) + } + + #[test] + fn a_pointer_payload_fits_one_tag_and_one_word() { + assert_eq!(size_of::>>(), 16); + assert_eq!(size_of::>(), 16); + } + + #[test] + fn statuses_meet_through_the_cell() { + assert_eq!(add(Poll::Final(1), Poll::Final(2)), Poll::Final(3)); + assert_eq!(add(Poll::Final(1), Poll::Partial(2)), Poll::Partial(3)); + assert_eq!(add(Poll::Pending, Poll::Final(2)), Poll::Pending); + + let Poll::Fallback(boxed) = add(Poll::Final(1), Poll::fallback(2, "boom")) else { unreachable!() }; + assert_eq!(boxed.0, 3); + assert_eq!(boxed.1.trace(), [1]); + + let Poll::Error(error) = add(Poll::Pending, Poll::error("boom")) else { unreachable!() }; + assert_eq!(error.trace(), [1]); + } + + #[test] + fn provisional_inputs_hold_kernel_errors_back() { + let parse = |text: Poll<&str>| { + Status::new().run(|cell| { + let text = cell.record(0, text)?; + text.parse::().map_err(|error| NodeError::new(error).into()) + }) + }; + assert_eq!(parse(Poll::Partial("")), Poll::Pending); + assert_eq!(parse(Poll::Partial("7")), Poll::Partial(7)); + assert!(matches!(parse(Poll::Final("")), Poll::Error(_))); + + assert_eq!(add(Poll::Partial(1), Poll::fallback(2, "boom")), Poll::Partial(3)); + + let Poll::Error(error) = add(Poll::Partial(1), Poll::error("boom")) else { unreachable!() }; + assert_eq!(error.trace(), [1]); + } + + #[test] + fn an_error_keeps_its_message() { + let Poll::Error(error) = add(Poll::Final(1), Poll::error(format!("no response from {}", "example.com"))) else { + unreachable!() + }; + assert_eq!(error.to_string(), "no response from example.com"); + + let parse_error = "x".parse::().unwrap_err(); + let Poll::Error(error) = add(Poll::error(parse_error), Poll::Final(2)) else { unreachable!() }; + assert_eq!(error.trace(), [0]); + let error: Box = error; + assert_eq!(error.to_string(), "invalid digit found in string"); + } + + #[test] + fn an_unrecorded_input_does_not_touch_the_status() { + let switch = |take_first: bool| { + Status::new().run(|cell| { + let first = || cell.record(0, Poll::Final(1)); + let second = || cell.record(1, Poll::Partial(2)); + if take_first { first() } else { second() } + }) + }; + assert_eq!(switch(true), Poll::Final(1)); + assert_eq!(switch(false), Poll::Partial(2)); + assert_eq!(Status::no_partial().run(|cell| cell.record(0, Poll::Partial(1))), Poll::Pending); + } +} From 45f0de42824dcdecee6f83373a7ccfd73c110187 Mon Sep 17 00:00:00 2001 From: Timon Date: Wed, 30 Sep 2026 15:35:48 +0000 Subject: [PATCH 18/54] Add source generations to context --- .../libraries/application-io/src/lib.rs | 6 ++- .../libraries/core-types/src/context.rs | 51 ++++++++++++++++++- node-graph/libraries/core-types/src/lib.rs | 1 + .../libraries/core-types/src/runtime.rs | 6 +++ node-graph/nodes/gstd/src/render_cache.rs | 6 ++- node-graph/nodes/gstd/src/render_node.rs | 1 + 6 files changed, 67 insertions(+), 4 deletions(-) diff --git a/node-graph/libraries/application-io/src/lib.rs b/node-graph/libraries/application-io/src/lib.rs index 3a6032e6df7..f06bb4eee1f 100644 --- a/node-graph/libraries/application-io/src/lib.rs +++ b/node-graph/libraries/application-io/src/lib.rs @@ -1,5 +1,5 @@ use core_types::transform::Footprint; -use core_types::{Context, OwnedContextImpl}; +use core_types::{Context, Generations, OwnedContextImpl}; use dyn_any::{DynAny, StaticType, StaticTypeSized}; use glam::DVec2; use graphene_hash::CacheHash; @@ -76,6 +76,10 @@ impl RenderConfig { pub fn into_context(self) -> Context<'static> { OwnedContextImpl::default().with_vararg(Box::new(self)).into_context() } + + pub fn into_context_with_generations(self, generations: Generations) -> Context<'static> { + OwnedContextImpl::default().with_vararg(Box::new(self)).with_generations(generations).into_context() + } } struct DummyPreferences; diff --git a/node-graph/libraries/core-types/src/context.rs b/node-graph/libraries/core-types/src/context.rs index ab8b0222393..4aba3e2a6fa 100644 --- a/node-graph/libraries/core-types/src/context.rs +++ b/node-graph/libraries/core-types/src/context.rs @@ -1,3 +1,4 @@ +use crate::runtime::{Generations, SourceId}; use crate::transform::Footprint; use glam::DVec2; pub use no_std_types::context::{ArcCtx, Ctx}; @@ -45,6 +46,10 @@ pub trait ExtractVarArgs { fn hash_varargs(&self, hasher: &mut dyn Hasher); } +pub trait ExtractGenerations { + fn try_generations(&self) -> Option<&Generations>; +} + pub trait CloneVarArgs: ExtractVarArgs { // TODO: Consider returning a slice or something like that @@ -77,7 +82,8 @@ pub trait ExtractAll: ExtractPointerPosition + ExtractPosition + ExtractIndex + - ExtractVarArgs {} + ExtractVarArgs + + ExtractGenerations {} impl< T: ?Sized // Extract traits @@ -87,7 +93,8 @@ impl< + ExtractPointerPosition + ExtractPosition + ExtractIndex - + ExtractVarArgs, + + ExtractVarArgs + + ExtractGenerations, > ExtractAll for T { } @@ -292,6 +299,12 @@ impl CloneVarArgs for Option { } } +impl ExtractGenerations for Option { + fn try_generations(&self) -> Option<&Generations> { + self.as_ref().and_then(|x| x.try_generations()) + } +} + // ================================ // EXTRACT TRAIT IMPLS FOR `Arc` // ================================ @@ -346,6 +359,12 @@ impl CloneVarArgs for Arc { } } +impl ExtractGenerations for Arc { + fn try_generations(&self) -> Option<&Generations> { + (**self).try_generations() + } +} + // ============================ // EXTRACT TRAIT IMPLS FOR `&T` // ============================ @@ -425,6 +444,12 @@ impl ExtractVarArgs for ContextImpl<'_> { } } +impl ExtractGenerations for ContextImpl<'_> { + fn try_generations(&self) -> Option<&Generations> { + None + } +} + // ========================================== // EXTRACT TRAIT IMPLS FOR `OwnedContextImpl` // ========================================== @@ -497,6 +522,12 @@ impl ExtractVarArgs for OwnedContextImpl { } } +impl ExtractGenerations for OwnedContextImpl { + fn try_generations(&self) -> Option<&Generations> { + self.generations.as_ref() + } +} + impl CloneVarArgs for Arc { fn arc_clone(&self) -> Option> { Some(self.clone()) @@ -522,6 +553,7 @@ pub struct OwnedContextImpl { // This could be converted into a single enum to save extra bytes index: Option>, varargs: Option>, + generations: Option, } impl std::fmt::Debug for OwnedContextImpl { @@ -534,6 +566,7 @@ impl std::fmt::Debug for OwnedContextImpl { .field("pointer_position", &self.pointer_position) .field("index", &self.index) .field("varargs_len", &self.varargs.as_ref().map(|x| x.len())) + .field("generations", &self.generations) .finish() } } @@ -553,6 +586,7 @@ impl graphene_hash::CacheHash for OwnedContextImpl { self.pointer_position.cache_hash(state); self.position.cache_hash(state); self.index.cache_hash(state); + self.generations.cache_hash(state); self.hash_varargs(state); } } @@ -588,6 +622,7 @@ impl OwnedContextImpl { position, index, varargs: None, + generations: value.try_generations().cloned(), } } @@ -601,6 +636,7 @@ impl OwnedContextImpl { position: None, index: None, varargs: None, + generations: None, } } } @@ -671,6 +707,17 @@ impl OwnedContextImpl { self.varargs = Some(Arc::new([value])); self } + pub fn with_generations(mut self, generations: Generations) -> Self { + self.generations = (!generations.is_empty()).then_some(generations); + self + } + pub fn without_source(mut self, source: SourceId) -> Self { + self.generations = self + .generations + .map(|generations| generations.iter().filter(|(id, _)| *id != source).copied().collect::()) + .filter(|generations| !generations.is_empty()); + self + } pub fn into_context(self) -> Option> { Some(Arc::new(self)) } diff --git a/node-graph/libraries/core-types/src/lib.rs b/node-graph/libraries/core-types/src/lib.rs index e6d76586aec..c1de01d88f0 100644 --- a/node-graph/libraries/core-types/src/lib.rs +++ b/node-graph/libraries/core-types/src/lib.rs @@ -41,6 +41,7 @@ pub use no_std_types::choice_type; pub use no_std_types::color; pub use no_std_types::shaders; pub use num_traits; +pub use runtime::{Generations, SourceId}; use std::any::TypeId; use std::future::Future; use std::pin::Pin; diff --git a/node-graph/libraries/core-types/src/runtime.rs b/node-graph/libraries/core-types/src/runtime.rs index b5e34ab0adb..ee1a6aa8eca 100644 --- a/node-graph/libraries/core-types/src/runtime.rs +++ b/node-graph/libraries/core-types/src/runtime.rs @@ -1,4 +1,5 @@ use std::pin::Pin; +use std::sync::Arc; #[cfg(not(target_family = "wasm"))] pub type Task = Pin + Send + 'static>>; @@ -8,3 +9,8 @@ pub type Task = Pin + 'static>>; pub trait Spawn { fn spawn(&self, task: Task); } + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, graphene_hash::CacheHash)] +pub struct SourceId(pub u64); + +pub type Generations = Arc<[(SourceId, u64)]>; diff --git a/node-graph/nodes/gstd/src/render_cache.rs b/node-graph/nodes/gstd/src/render_cache.rs index cccc3319eec..cf8b93ac7d2 100644 --- a/node-graph/nodes/gstd/src/render_cache.rs +++ b/node-graph/nodes/gstd/src/render_cache.rs @@ -3,7 +3,7 @@ use core_types::list::Item; use core_types::math::bbox::AxisAlignedBbox; use core_types::transform::{Footprint, RenderQuality, Transform}; -use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime, OwnedContextImpl}; +use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime, Generations, OwnedContextImpl}; use glam::{DAffine2, DVec2, IVec2, UVec2}; use graph_craft::application_io::PlatformEditorApi; use graph_craft::document::value::{RenderOutput, RenderOutputType}; @@ -48,6 +48,7 @@ pub struct CacheKey { pub animation_time_ms: i64, pub real_time_ms: i64, pub pointer: [u8; 16], + pub generations: Generations, } impl CacheKey { @@ -65,6 +66,7 @@ impl CacheKey { animation_time: f64, real_time: f64, pointer: Option, + generations: Generations, ) -> Self { let pointer_bytes = pointer .map(|p| { @@ -90,6 +92,7 @@ impl CacheKey { animation_time_ms: (animation_time * 1000.).round() as i64, real_time_ms: (real_time * 1000.).round() as i64, pointer: pointer_bytes, + generations, } } } @@ -367,6 +370,7 @@ async fn render_output_cache( ctx.try_animation_time().unwrap_or(0.), ctx.try_real_time().unwrap_or(0.), ctx.try_pointer_position(), + ctx.try_generations().cloned().unwrap_or_default(), ); let cache_query = tile_cache.query(&viewport_bounds_device, &cache_key, max_region_area); diff --git a/node-graph/nodes/gstd/src/render_node.rs b/node-graph/nodes/gstd/src/render_node.rs index 54c4f1f6309..ac12e494eb5 100644 --- a/node-graph/nodes/gstd/src/render_node.rs +++ b/node-graph/nodes/gstd/src/render_node.rs @@ -182,6 +182,7 @@ async fn create_context( .with_animation_time(render_config.time.animation_time.as_secs_f64()) .with_pointer_position(render_config.pointer) .with_vararg(Box::new(render_params)) + .with_generations(ctx.try_generations().cloned().unwrap_or_default()) .into_context(); let mut result = data.eval(ctx).await; From a5703b4522114fd1081af816ca315de09e4f92a8 Mon Sep 17 00:00:00 2001 From: Timon Date: Sat, 10 Oct 2026 13:06:51 +0000 Subject: [PATCH 19/54] Stop storing context features in the document --- document/graph-storage/src/attributes/attr.rs | 1 - document/graph-storage/src/from_runtime.rs | 9 -- .../graph-storage/src/tests/round_trip.rs | 15 ---- document/graph-storage/src/to_runtime.rs | 2 +- .../portfolio/document/document_diff.rs | 4 +- .../document_node_derive.rs | 2 - .../utility_types/network_interface.rs | 1 - .../utility_types/network_interface/apply.rs | 10 --- .../network_interface/editor_delta.rs | 11 --- .../network_interface/mutations.rs | 8 -- .../network_interface/store/cursor.rs | 15 ---- .../network_interface/template.rs | 9 +- .../messages/portfolio/document_migration.rs | 9 -- node-graph/graph-craft/src/document.rs | 12 +-- node-graph/graph-craft/src/proto.rs | 18 ++-- node-graph/graph-craft/src/runtime_delta.rs | 8 +- node-graph/interpreted-executor/src/util.rs | 24 ++--- node-graph/preprocessor/src/lib.rs | 89 +++++++++++++++++-- 18 files changed, 113 insertions(+), 134 deletions(-) diff --git a/document/graph-storage/src/attributes/attr.rs b/document/graph-storage/src/attributes/attr.rs index 485f04dfe2f..5621a5103da 100644 --- a/document/graph-storage/src/attributes/attr.rs +++ b/document/graph-storage/src/attributes/attr.rs @@ -1,6 +1,5 @@ pub mod node { pub const CALL_ARGUMENT: &str = "call_argument"; - pub const CONTEXT_FEATURES: &str = "context_features"; pub const VISIBLE: &str = "visible"; pub const SKIP_DEDUPLICATION: &str = "skip_deduplication"; pub const REFLECTION_METADATA: &str = "reflection_metadata"; diff --git a/document/graph-storage/src/from_runtime.rs b/document/graph-storage/src/from_runtime.rs index 4b8d533fcea..3affc6de870 100644 --- a/document/graph-storage/src/from_runtime.rs +++ b/document/graph-storage/src/from_runtime.rs @@ -1,7 +1,6 @@ use std::collections::HashMap; use core_types::Context; -use core_types::context::ContextDependencies; use core_types::uuid::NodeId as RuntimeNodeId; use graph_craft::concrete; use graph_craft::document::value::TaggedValue; @@ -436,9 +435,6 @@ fn convert_node( attributes .set_if_not_default(node::CALL_ARGUMENT, &doc_node.call_argument, &concrete!(Context), timestamp) .map_err(map_serialization_error(node::CALL_ARGUMENT))?; - attributes - .set_if_not_default(node::CONTEXT_FEATURES, &doc_node.context_features, &ContextDependencies::default(), timestamp) - .map_err(map_serialization_error(node::CONTEXT_FEATURES))?; attributes .set_if_not_default(node::VISIBLE, &doc_node.visible, &true, timestamp) .map_err(map_serialization_error(node::VISIBLE))?; @@ -463,11 +459,6 @@ pub fn encode_call_argument(call_argument: &core_types::Type) -> Result Result { - encode_if_not_default(node::CONTEXT_FEATURES, context_features, &ContextDependencies::default()) -} - fn encode_if_not_default(key: &str, value: &T, default: &T) -> Result { let value = if value == default { None } else { Some(to_value(value).map_err(map_serialization_error(key))?) }; diff --git a/document/graph-storage/src/tests/round_trip.rs b/document/graph-storage/src/tests/round_trip.rs index 36a85bc12d4..8b43f5270f4 100644 --- a/document/graph-storage/src/tests/round_trip.rs +++ b/document/graph-storage/src/tests/round_trip.rs @@ -1,7 +1,6 @@ use std::borrow::Cow; use std::collections::HashMap; -use core_types::context::ContextDependencies; use core_types::uuid::NodeId; use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeInput, NodeNetwork}; use graph_craft::graphene_compiler::Compiler; @@ -244,11 +243,6 @@ fn test_nested_network_flattening() { #[test] fn test_metadata_preservation() { // Create a network with nodes that have non-default metadata - let context_features = ContextDependencies { - extract: core_types::context::ContextFeatures::FOOTPRINT | core_types::context::ContextFeatures::REAL_TIME, - ..Default::default() - }; - let network = NodeNetwork { exports: vec![NodeInput::node(NodeId(1), 0)], nodes: [ @@ -258,7 +252,6 @@ fn test_metadata_preservation() { inputs: vec![NodeInput::import(concrete!(f64), 0), NodeInput::import(Type::Generic(Cow::Borrowed("T")), 1)], implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("test::NodeWithMetadata")), call_argument: concrete!(String), - context_features, visible: false, // Non-default value skip_deduplication: true, // Non-default value ..Default::default() @@ -292,9 +285,6 @@ fn test_metadata_preservation() { let conv_node_1 = converted.nodes.get(&NodeId(1)).unwrap(); assert_eq!(orig_node_1.call_argument, conv_node_1.call_argument, "call_argument for node 1 should be preserved"); - // Verify context_features is preserved - assert_eq!(orig_node_0.context_features, conv_node_0.context_features, "context_features should be preserved"); - // Verify visible is preserved assert_eq!(orig_node_0.visible, conv_node_0.visible, "visible should be preserved"); @@ -353,11 +343,6 @@ fn test_demo_artwork_round_trip() { "{}: call_argument should be preserved for node {:?}", artwork_name, node_id ); - assert_eq!( - orig_node.context_features, conv_node.context_features, - "{}: context_features should be preserved for node {:?}", - artwork_name, node_id - ); assert_eq!(orig_node.visible, conv_node.visible, "{}: visible should be preserved for node {:?}", artwork_name, node_id); assert_eq!( orig_node.skip_deduplication, conv_node.skip_deduplication, diff --git a/document/graph-storage/src/to_runtime.rs b/document/graph-storage/src/to_runtime.rs index 6845f5ae278..395f1acafb2 100644 --- a/document/graph-storage/src/to_runtime.rs +++ b/document/graph-storage/src/to_runtime.rs @@ -326,8 +326,8 @@ fn convert_node( implementation: convert_implementation(context, &node.implementation, metadata_path, runtime_node_id, node_collector, network_collector)?, visible: node.attributes.get_or(node::VISIBLE, true), skip_deduplication: node.attributes.get_or(node::SKIP_DEDUPLICATION, false), - context_features: node.attributes.get_or_default(node::CONTEXT_FEATURES), // Regenerated during compilation; not stored. + context_dependencies: None, original_location: Default::default(), }) } diff --git a/editor/src/messages/portfolio/document/document_diff.rs b/editor/src/messages/portfolio/document/document_diff.rs index 2153a5d4b11..62eee131536 100644 --- a/editor/src/messages/portfolio/document/document_diff.rs +++ b/editor/src/messages/portfolio/document/document_diff.rs @@ -266,8 +266,8 @@ fn diff_document_node(out: &mut String, expected: &graph_craft::document::Docume if expected.skip_deduplication != actual.skip_deduplication { let _ = writeln!(out, " skip_deduplication: expected={} actual={}", expected.skip_deduplication, actual.skip_deduplication); } - if expected.context_features != actual.context_features { - let _ = writeln!(out, " context_features: expected={:?} actual={:?}", expected.context_features, actual.context_features); + if expected.context_dependencies != actual.context_dependencies { + let _ = writeln!(out, " context_dependencies: expected={:?} actual={:?}", expected.context_dependencies, actual.context_dependencies); } if expected.original_location != actual.original_location { let _ = writeln!(out, " original_location differs (recomputed at load, not stored):"); diff --git a/editor/src/messages/portfolio/document/node_graph/document_node_definitions/document_node_derive.rs b/editor/src/messages/portfolio/document/node_graph/document_node_definitions/document_node_derive.rs index f8a96b0c8c7..2f2e1c6ad5a 100644 --- a/editor/src/messages/portfolio/document/node_graph/document_node_definitions/document_node_derive.rs +++ b/editor/src/messages/portfolio/document/node_graph/document_node_definitions/document_node_derive.rs @@ -41,7 +41,6 @@ pub(super) fn post_process_nodes(custom: Vec) -> HashMap fields, description, properties, - context_features, .. } = metadata; @@ -74,7 +73,6 @@ pub(super) fn post_process_nodes(custom: Vec) -> HashMap inputs, call_argument: input_type.clone(), implementation: NodeTemplateImplementation::ProtoNode(id.clone()), - context_features: ContextDependencies::from(context_features.as_slice()), // TODO: Store information for input overrides in the node macro input_metadata: fields .iter() diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface.rs b/editor/src/messages/portfolio/document/utility_types/network_interface.rs index d7c23ba7501..af15c2e228c 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface.rs @@ -52,7 +52,6 @@ use graph_craft::application_io::resource::ResourceId; use graph_craft::document::value::TaggedValue; use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput, NodeNetwork, OldDocumentNodeImplementation, OldNodeNetwork}; use graphene_std::Appearance; -use graphene_std::ContextDependencies; use graphene_std::math::quad::Quad; use graphene_std::transform::Footprint; use graphene_std::vector::click_target::{ClickTarget, ClickTargetType, FreePoint}; diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface/apply.rs b/editor/src/messages/portfolio/document/utility_types/network_interface/apply.rs index 40c7d6ac7a3..6fa429bc046 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface/apply.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface/apply.rs @@ -73,16 +73,6 @@ impl NodeNetworkInterface { } } - EditorDelta::Graph(RuntimeDelta::SetContextFeatures { - network_path, - node_id, - context_features, - }) => { - if let Some(mut node) = self.node_mut(NodeLocator::new(*node_id, network_path)) { - node.set_context_features(*context_features); - } - } - EditorDelta::NodeMetadataSnapshot { network_path, node_id, metadata } => { if let Some(mut node) = self.node_mut(NodeLocator::new(*node_id, network_path)) { node.replace_metadata((**metadata).clone()); diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface/editor_delta.rs b/editor/src/messages/portfolio/document/utility_types/network_interface/editor_delta.rs index 538d180d40f..27d4d2c6643 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface/editor_delta.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface/editor_delta.rs @@ -209,17 +209,6 @@ impl EditorDelta { }); } - EditorDelta::Graph(RuntimeDelta::SetContextFeatures { - network_path, - node_id, - context_features, - }) => { - ops.push(RegistryDelta::ChangeNodeAttribute { - id: context.resolver.node_id(network_path, *node_id), - delta: document_graph_storage::from_runtime::encode_context_features(context_features)?, - }); - } - EditorDelta::NodeMetadataSnapshot { network_path, node_id, metadata } => { construct_metadata_snapshot(network_path, *node_id, metadata, batch, &context.resolver, ops)?; } diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface/mutations.rs b/editor/src/messages/portfolio/document/utility_types/network_interface/mutations.rs index fc8a47c54e0..585d4ac292e 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface/mutations.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface/mutations.rs @@ -446,14 +446,6 @@ impl NodeNetworkInterface { node.set_call_argument(call_argument); } - pub fn set_context_features(&mut self, node_id: &NodeId, network_path: &[NodeId], context_features: ContextDependencies) { - let Some(mut node) = self.node_mut(NodeLocator::new(*node_id, network_path)) else { - log::error!("Could not get node {node_id} in set_context_features"); - return; - }; - node.set_context_features(context_features); - } - /// Lightweight version of `set_input` for bulk import operations. /// Directly sets the input without `is_acyclic` checks, `load_structure`, position conversions, /// or per-node cache invalidation. Call `load_structure`, `unload_all_nodes_click_targets`, and diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface/store/cursor.rs b/editor/src/messages/portfolio/document/utility_types/network_interface/store/cursor.rs index 28170545744..d16f13c194a 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface/store/cursor.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface/store/cursor.rs @@ -193,21 +193,6 @@ impl NodeMut<'_> { changed } - /// The Extract and Inject annotations the node declares for the Context. - pub(crate) fn set_context_features(&mut self, context_features: ContextDependencies) -> bool { - let changed = self.node.context_features != context_features; - self.node.context_features = context_features; - if changed { - let delta = RuntimeDelta::SetContextFeatures { - network_path: self.deltas.network_path(), - node_id: self.deltas.node_id(), - context_features: self.node.context_features, - }; - self.deltas.graph(delta); - } - changed - } - /// The user-chosen name for this instance, empty when it has none. pub(crate) fn set_display_name(&mut self, display_name: String) -> bool { let changed = self.metadata.display_name != display_name; diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface/template.rs b/editor/src/messages/portfolio/document/utility_types/network_interface/template.rs index 57fe1d2e1c4..42f1e89e118 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface/template.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface/template.rs @@ -23,8 +23,6 @@ pub struct NodeTemplate { pub visible: bool, /// Prevents identical proto nodes from being deduplicated during compilation, e.g. for monitor nodes. pub skip_deduplication: bool, - /// List of Extract and Inject annotations for the Context. - pub context_features: ContextDependencies, /// A name chosen by the user for this instance of the node. Empty indicates no given name, in which case the implementation name is displayed in italics. pub display_name: String, /// Metadata to override the properties panel widgets for each input. Kept the same length as `inputs`. @@ -58,7 +56,6 @@ impl Default for NodeTemplate { implementation: NodeTemplateImplementation::default(), visible: true, skip_deduplication: false, - context_features: ContextDependencies::default(), display_name: String::new(), input_metadata: Vec::new(), output_names: Vec::new(), @@ -114,7 +111,7 @@ impl NodeTemplate { implementation, visible, skip_deduplication, - context_features, + context_dependencies: _, original_location: _, } = document_node; let DocumentNodePersistentMetadata { @@ -159,7 +156,6 @@ impl NodeTemplate { implementation, visible, skip_deduplication, - context_features, display_name, input_metadata, output_names, @@ -177,7 +173,6 @@ impl NodeTemplate { implementation, visible, skip_deduplication, - context_features, display_name, mut input_metadata, output_names, @@ -197,7 +192,7 @@ impl NodeTemplate { implementation, visible, skip_deduplication, - context_features, + context_dependencies: None, original_location: Default::default(), }; let persistent_node_metadata = DocumentNodePersistentMetadata { diff --git a/editor/src/messages/portfolio/document_migration.rs b/editor/src/messages/portfolio/document_migration.rs index f58b5c0f4c4..3960d68de54 100644 --- a/editor/src/messages/portfolio/document_migration.rs +++ b/editor/src/messages/portfolio/document_migration.rs @@ -2980,15 +2980,6 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], } } - // Add context features to nodes that don't have them (fine-grained context caching migration) - if node.context_features == graphene_std::ContextDependencies::default() - && let Some(reference) = document.network_interface.reference(node_id, network_path).clone() - && let Some(node_definition) = resolve_document_node_type(&reference) - { - let context_features = node_definition.node_template.context_features; - document.network_interface.set_context_features(node_id, network_path, context_features); - } - // Add the "Scale Type" parameter to the "Decompose Scale" node if reference == DefinitionIdentifier::ProtoNode(graphene_std::transform_nodes::decompose_scale::IDENTIFIER) && inputs_count == 1 { let mut node_template = resolve_document_node_type(&reference)?.default_node_template(); diff --git a/node-graph/graph-craft/src/document.rs b/node-graph/graph-craft/src/document.rs index 9492a4839aa..1c87cd5f98b 100644 --- a/node-graph/graph-craft/src/document.rs +++ b/node-graph/graph-craft/src/document.rs @@ -57,9 +57,8 @@ pub struct DocumentNode { /// However sometimes this is not desirable, for example in the case of a [`graphene_core::memo::MonitorNode`] that needs to be accessed outside of the graph. #[serde(default)] pub skip_deduplication: bool, - /// List of Extract and Inject annotations for the Context. - #[serde(default)] - pub context_features: ContextDependencies, + #[serde(skip)] + pub context_dependencies: Option, /// The path to this node and its inputs and outputs as of when [`NodeNetwork::generate_node_paths`] was called. #[serde(skip)] pub original_location: OriginalLocation, @@ -97,7 +96,7 @@ impl Default for DocumentNode { visible: true, skip_deduplication: Default::default(), original_location: OriginalLocation::default(), - context_features: Default::default(), + context_dependencies: Default::default(), } } } @@ -193,7 +192,7 @@ impl DocumentNode { construction_args, original_location: self.original_location, skip_deduplication: self.skip_deduplication, - context_features: self.context_features, + context_dependencies: self.context_dependencies.unwrap_or_default(), } } } @@ -957,6 +956,7 @@ impl NodeNetwork { self.nodes.insert(id, node); return; }; + debug_assert!(node.context_dependencies.is_none(), "Network {id} should not have context dependencies: {node:#?}"); // Replace value and reflection imports with value nodes, added inside nested network Self::replace_value_inputs_with_nodes( @@ -1289,7 +1289,7 @@ impl core_types::graphene_hash::CacheHash for DocumentNode { self.implementation.cache_hash(state); self.visible.cache_hash(state); self.skip_deduplication.cache_hash(state); - self.context_features.cache_hash(state); + self.context_dependencies.cache_hash(state); } } diff --git a/node-graph/graph-craft/src/proto.rs b/node-graph/graph-craft/src/proto.rs index 6a4d5a436df..5377ded9fb6 100644 --- a/node-graph/graph-craft/src/proto.rs +++ b/node-graph/graph-craft/src/proto.rs @@ -133,7 +133,7 @@ pub struct ProtoNode { pub identifier: ProtoNodeIdentifier, pub original_location: OriginalLocation, pub skip_deduplication: bool, - pub(crate) context_features: ContextDependencies, + pub(crate) context_dependencies: ContextDependencies, } impl Default for ProtoNode { @@ -144,7 +144,7 @@ impl Default for ProtoNode { call_argument: concrete!(()), original_location: OriginalLocation::default(), skip_deduplication: false, - context_features: Default::default(), + context_dependencies: Default::default(), } } } @@ -184,7 +184,7 @@ impl ProtoNode { ..Default::default() }, skip_deduplication: false, - context_features: Default::default(), + context_dependencies: Default::default(), } } @@ -370,13 +370,13 @@ impl ProtoNetwork { let mut combined_deps = ContextFeatures::default(); let node_index = id.0 as usize; - let context_features = self.nodes[node_index].1.context_features; + let dependencies = self.nodes[node_index].1.context_dependencies; let mut inputs = match &self.nodes[node_index].1.construction_args { // We pretend like we have already placed context modification nodes after ourselves because value nodes don't need to be cached - ConstructionArgs::Value(_) => return (context_features.extract, Some(id)), + ConstructionArgs::Value(_) => return (dependencies.extract, Some(id)), ConstructionArgs::Nodes(items) => items.clone(), - ConstructionArgs::Inline(_) => return (context_features.extract, Some(id)), + ConstructionArgs::Inline(_) => return (dependencies.extract, Some(id)), }; // Compute the dependencies for each branch and combine all of them @@ -389,9 +389,9 @@ impl ProtoNetwork { let mut new_deps = combined_deps; // Remove requirements which this node provides - new_deps &= !context_features.inject; + new_deps &= !dependencies.inject; // Add requirements we have - new_deps |= context_features.extract; + new_deps |= dependencies.extract; // If we either introduce new dependencies, we can cache all children which don't yet need that dependency let we_introduce_new_deps = !combined_deps.contains(new_deps); @@ -407,7 +407,7 @@ impl ProtoNetwork { self.nodes[node_index].1.construction_args = ConstructionArgs::Nodes(inputs); // Which dependencies do we supply (and don't need ourselves)? - let net_injections = context_features.inject.difference(context_features.extract); + let net_injections = dependencies.inject.difference(dependencies.extract); // Which dependencies still need to be met after this node? let remaining_deps_from_children = combined_deps.difference(net_injections); diff --git a/node-graph/graph-craft/src/runtime_delta.rs b/node-graph/graph-craft/src/runtime_delta.rs index f29ca974435..88dcb2fdc07 100644 --- a/node-graph/graph-craft/src/runtime_delta.rs +++ b/node-graph/graph-craft/src/runtime_delta.rs @@ -1,5 +1,5 @@ use crate::document::{DocumentNode, NodeId, NodeInput}; -use core_types::{ContextDependencies, Type}; +use core_types::Type; /// One mutation's worth of structural graph change, carrying its post-change data as plain runtime /// types. Constructed by the mutation itself; a compound mutation emits several. Consumed typed and @@ -60,10 +60,4 @@ pub enum RuntimeDelta { node_id: NodeId, call_argument: Type, }, - /// The Extract and Inject annotations the node declares for the Context, as [`ContextDependencies`]. - SetContextFeatures { - network_path: Vec, - node_id: NodeId, - context_features: ContextDependencies, - }, } diff --git a/node-graph/interpreted-executor/src/util.rs b/node-graph/interpreted-executor/src/util.rs index 612cdaba5ce..3cc374d495f 100644 --- a/node-graph/interpreted-executor/src/util.rs +++ b/node-graph/interpreted-executor/src/util.rs @@ -30,20 +30,20 @@ pub fn wrap_network_in_scope(network: NodeNetwork, editor_api: Arc, + substitutions: HashMap, inject_scopes: HashMap, + context_dependencies: HashMap, } impl Preprocessor { @@ -94,7 +101,9 @@ impl Preprocessor { match &mut node.implementation { DocumentNodeImplementation::Network(node_network) => self.expand_network(node_network), DocumentNodeImplementation::ProtoNode(proto_node_identifier) => { - if let Some(new_node) = self.substitutions.get(proto_node_identifier) { + let context_dependencies = node.context_dependencies.take().or_else(|| self.context_dependencies.get(proto_node_identifier).copied()); + + if let Some(Substitution { node: new_node, wrapped_node_id }) = self.substitutions.get(proto_node_identifier) { // Reconcile the document node's inputs with what the current node definition expects, // since the saved document may have fewer or more inputs than the current version while node.inputs.len() < new_node.inputs.len() { @@ -103,6 +112,15 @@ impl Preprocessor { node.inputs.truncate(new_node.inputs.len()); node.implementation = new_node.implementation.clone(); + + // Move context dependencies to the wrapped node because networks are not allowed to have context dependencies so we need to + if let DocumentNodeImplementation::Network(network) = &mut node.implementation + && let Some(wrapped_node) = network.nodes.get_mut(wrapped_node_id) + { + wrapped_node.context_dependencies = context_dependencies; + } + } else { + node.context_dependencies = context_dependencies; } } DocumentNodeImplementation::Extract => (), @@ -112,6 +130,7 @@ impl Preprocessor { pub fn new() -> Self { let mut substitutions = HashMap::new(); + let mut context_dependencies = HashMap::new(); let mut inject_scopes = HashMap::new(); // We pre initialize the node registry here to avoid a deadlock let into_node_registry = &*interpreted_executor::node_registry::NODE_REGISTRY; @@ -120,6 +139,8 @@ impl Preprocessor { let id = id.clone(); let NodeMetadata { fields, memoize, inject_scope, .. } = metadata; + context_dependencies.insert(id.clone(), ContextDependencies::from(metadata.context_features.as_slice())); + let Some(implementations) = node_registry.get(&id) else { continue }; let valid_call_args: HashSet<_> = implementations.iter().map(|(_, node_io)| node_io.call_argument.clone()).collect(); let first_node_io = implementations.first().map(|(_, node_io)| node_io).unwrap_or(const { &NodeIOTypes::empty() }); @@ -242,17 +263,17 @@ impl Preprocessor { implementation: DocumentNodeImplementation::ProtoNode(id.clone()), visible: true, skip_deduplication: false, - context_features: ContextDependencies::from(metadata.context_features.as_slice()), ..Default::default() }; - nodes.insert(NodeId(input_count as u64), document_node); + let wrapped_node_id = NodeId(input_count as u64); + nodes.insert(wrapped_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 = DocumentNode { - inputs: vec![NodeInput::node(NodeId(input_count as u64), 0)], + inputs: vec![NodeInput::node(wrapped_node_id, 0)], implementation: DocumentNodeImplementation::ProtoNode(graphene_core::memo::memoize::IDENTIFIER.clone()), visible: true, ..Default::default() @@ -260,7 +281,7 @@ impl Preprocessor { nodes.insert(memoize_node_id, memoize_node); memoize_node_id } else { - NodeId(input_count as u64) + wrapped_node_id }; let node = DocumentNode { @@ -280,7 +301,7 @@ impl Preprocessor { ..Default::default() }; - substitutions.insert(id.clone(), node); + substitutions.insert(id.clone(), Substitution { node, wrapped_node_id }); // If `inject_scope` is requested, prepare the proto node template and type info needed if *inject_scope @@ -292,14 +313,17 @@ impl Preprocessor { call_argument: node_io.call_argument.clone(), implementation: DocumentNodeImplementation::ProtoNode(id.clone()), visible: true, - context_features: ContextDependencies::from(metadata.context_features.as_slice()), ..Default::default() }; inject_scopes.insert(id.clone(), (template, node_io.return_value.clone())); } } - Self { substitutions, inject_scopes } + Self { + substitutions, + context_dependencies, + inject_scopes, + } } } @@ -417,4 +441,51 @@ mod tests { "The paint input's `Color::BLACK` default should parse against its `Item` wire type" ); } + + #[test] + fn substitution_moves_context_dependencies_onto_the_wrapped_node() { + let identifier = graphene_std::path_bool_nodes::boolean_operation::IDENTIFIER; + let context_dependencies = ContextDependencies { + extract: ContextFeatures::FOOTPRINT, + inject: ContextFeatures::INDEX, + }; + let mut network = NodeNetwork { + nodes: [( + NodeId(0), + DocumentNode { + implementation: DocumentNodeImplementation::ProtoNode(identifier.clone()), + context_dependencies: Some(context_dependencies), + ..Default::default() + }, + )] + .into_iter() + .collect(), + ..Default::default() + }; + + Preprocessor::new().expand_network(&mut network); + + let node = &network.nodes[&NodeId(0)]; + assert_eq!( + node.context_dependencies, None, + "The network node replacing the substituted node should not keep its context dependencies" + ); + let DocumentNodeImplementation::Network(substitution) = &node.implementation else { + panic!("Boolean Operation is memoized, so it should be substituted with a network"); + }; + + let wrapped_node = substitution + .nodes + .values() + .find(|inner| inner.implementation == DocumentNodeImplementation::ProtoNode(identifier.clone())) + .expect("The substitution network should contain the wrapped proto node"); + assert_eq!(wrapped_node.context_dependencies, Some(context_dependencies), "The wrapped proto node should carry the override"); + + let NodeInput::Node { node_id: export_id, .. } = substitution.exports[0] else { + panic!("The substitution network should export a node"); + }; + let export = &substitution.nodes[&export_id]; + assert_eq!(export.implementation, DocumentNodeImplementation::ProtoNode(graphene_core::memo::memoize::IDENTIFIER)); + assert_eq!(export.context_dependencies, None, "The memoize node should not take over the wrapped node's context dependencies"); + } } From c2de87ebc51d013b97174c5b495e910a51f62bf4 Mon Sep 17 00:00:00 2001 From: Timon Date: Sat, 10 Oct 2026 13:07:02 +0000 Subject: [PATCH 20/54] Track source ids in context modification --- node-graph/graph-craft/src/document.rs | 9 +- node-graph/graph-craft/src/document/value.rs | 21 ++- node-graph/graph-craft/src/proto.rs | 141 +++++++++++++++--- .../src/dynamic_executor/test.rs | 2 +- .../interpreted-executor/src/node_registry.rs | 22 +-- .../libraries/core-types/src/context.rs | 27 ++++ .../libraries/core-types/src/registry.rs | 1 + .../libraries/core-types/src/runtime.rs | 12 +- node-graph/libraries/graphene-hash/src/lib.rs | 11 ++ .../nodes/gcore/src/context_modification.rs | 7 +- node-graph/preprocessor/src/lib.rs | 1 + 11 files changed, 211 insertions(+), 43 deletions(-) diff --git a/node-graph/graph-craft/src/document.rs b/node-graph/graph-craft/src/document.rs index 1c87cd5f98b..e565de2077e 100644 --- a/node-graph/graph-craft/src/document.rs +++ b/node-graph/graph-craft/src/document.rs @@ -241,12 +241,14 @@ impl InlineRust { #[derive(Debug, Clone, PartialEq, Hash, core_types::CacheHash, DynAny, serde::Serialize, serde::Deserialize)] pub enum DocumentNodeMetadata { DocumentNodePath, + SourceId, } impl DocumentNodeMetadata { pub fn ty(&self) -> Type { match self { DocumentNodeMetadata::DocumentNodePath => item!(core_types::list::NodeIdPath), + DocumentNodeMetadata::SourceId => item!(core_types::SourceId), } } } @@ -956,7 +958,11 @@ impl NodeNetwork { self.nodes.insert(id, node); return; }; - debug_assert!(node.context_dependencies.is_none(), "Network {id} should not have context dependencies: {node:#?}"); + debug_assert!( + node.context_dependencies.is_none(), + "Network {id} should not have context dependencies: {deps:#?}", + deps = node.context_dependencies + ); // Replace value and reflection imports with value nodes, added inside nested network Self::replace_value_inputs_with_nodes( @@ -1064,6 +1070,7 @@ impl NodeNetwork { NodeInput::Value { tagged_value, exposed } => (tagged_value, exposed), NodeInput::Reflection(reflect) => match reflect { DocumentNodeMetadata::DocumentNodePath => (TaggedValue::NodeIdPath(core_types::list::NodeIdPath::from(path.to_vec())).into(), false), + DocumentNodeMetadata::SourceId => (TaggedValue::SourceId(core_types::SourceId::from_path(path)).into(), false), }, previous_export => { *export = previous_export; diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 9dbc08b4db7..dd821afa37b 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -7,7 +7,7 @@ use core_types::color::SRGBA8; use core_types::list::{Item, List, NodeIdPath}; use core_types::transfer_curve::TransferCurve; use core_types::transform::Footprint; -use core_types::{CacheHash, Color, ContextFeatures, MemoHash, Node, Type, TypeDescriptor}; +use core_types::{CacheHash, Color, ContextModification, MemoHash, Node, SourceId, Type, TypeDescriptor}; use dyn_any::DynAny; pub use dyn_any::StaticType; pub use glam::{DAffine2, DVec2, IVec2, UVec2}; @@ -114,7 +114,9 @@ macro_rules! tagged_value { DocumentNode(DocumentNode), /// Carried by context nullification proto nodes constructed at proto node compilation time in `insert_context_nullification_nodes`. #[serde(skip)] - ContextFeatures(ContextFeatures), + ContextModification(ContextModification), + #[serde(skip)] + SourceId(SourceId), #[serde(skip)] EditorApi(Arc), /// Only used by the `resource` node, should never be serialized @@ -147,7 +149,8 @@ macro_rules! tagged_value { // ======================= Self::NodeIdPath(path) => path.cache_hash(state), Self::DocumentNode(node) => node.cache_hash(state), - Self::ContextFeatures(features) => features.cache_hash(state), + Self::ContextModification(modification) => modification.cache_hash(state), + Self::SourceId(source) => source.cache_hash(state), Self::RenderOutput(x) => x.cache_hash(state), Self::EditorApi(x) => x.cache_hash(state), Self::ResourceHash(x) => x.cache_hash(state), @@ -220,7 +223,8 @@ macro_rules! tagged_value { Self::RenderOutput(x) => Box::new(Item::new_from_element(x)), Self::NodeIdPath(path) => Box::new(Item::new_from_element(path)), Self::DocumentNode(node) => Box::new(node), - Self::ContextFeatures(features) => Box::new(Item::new_from_element(features)), + Self::ContextModification(modification) => Box::new(Item::new_from_element(modification)), + Self::SourceId(source) => Box::new(Item::new_from_element(source)), Self::EditorApi(x) => Box::new(Item::new_from_element(x)), Self::ResourceHash(x) => Box::new(Item::new_from_element(x)), } @@ -288,7 +292,8 @@ macro_rules! tagged_value { Self::RenderOutput(x) => Arc::new(Item::new_from_element(x)), Self::NodeIdPath(path) => Arc::new(Item::new_from_element(path)), Self::DocumentNode(node) => Arc::new(node), - Self::ContextFeatures(features) => Arc::new(Item::new_from_element(features)), + Self::ContextModification(modification) => Arc::new(Item::new_from_element(modification)), + Self::SourceId(source) => Arc::new(Item::new_from_element(source)), Self::EditorApi(x) => Arc::new(Item::new_from_element(x)), Self::ResourceHash(x) => Arc::new(Item::new_from_element(x)), } @@ -319,7 +324,8 @@ macro_rules! tagged_value { Self::RenderOutput(_) => item!(RenderOutput), Self::NodeIdPath(_) => item!(NodeIdPath), Self::DocumentNode(_) => concrete!(DocumentNode), - Self::ContextFeatures(_) => item!(ContextFeatures), + Self::ContextModification(_) => item!(ContextModification), + Self::SourceId(_) => item!(SourceId), Self::EditorApi(_) => item!(Arc), Self::ResourceHash(_) => item!(ResourceHash), }; @@ -485,7 +491,8 @@ macro_rules! tagged_value { Self::RenderOutput(_) => "RenderOutput".to_string(), Self::NodeIdPath(path) => format!("NodeIdPath({path:?})"), Self::DocumentNode(node) => format!("DocumentNode({node:?})"), - Self::ContextFeatures(features) => format!("ContextFeatures({features:?})"), + Self::ContextModification(modification) => format!("ContextModification({modification:?})"), + Self::SourceId(source) => format!("SourceId({source:?})"), Self::EditorApi(_) => "PlatformEditorApi".to_string(), Self::ResourceHash(hash) => format!("ResourceHash({hash:?})"), } diff --git a/node-graph/graph-craft/src/proto.rs b/node-graph/graph-craft/src/proto.rs index 5377ded9fb6..5181e8fcbd2 100644 --- a/node-graph/graph-craft/src/proto.rs +++ b/node-graph/graph-craft/src/proto.rs @@ -308,7 +308,7 @@ impl ProtoNetwork { Ok(()) } - fn insert_context_nullification_node(&mut self, node_id: NodeId, context_deps: ContextFeatures) -> NodeId { + fn insert_context_nullification_node(&mut self, node_id: NodeId, context_deps: ContextModification) -> NodeId { let (_, node) = &self.nodes[node_id.0 as usize]; let mut path = node.original_location.path.clone(); @@ -338,7 +338,7 @@ impl ProtoNetwork { self.nodes.push(( nullification_value_node_id, ProtoNode { - construction_args: ConstructionArgs::Value(MemoHash::new(TaggedValue::ContextFeatures(context_deps))), + construction_args: ConstructionArgs::Value(MemoHash::new(TaggedValue::ContextModification(context_deps))), call_argument: concrete!(Context), identifier: ProtoNodeIdentifier::new("core_types::value::ClonedNode"), original_location: OriginalLocation { @@ -365,42 +365,57 @@ impl ProtoNetwork { nullification_node_id } - fn find_context_dependencies(&mut self, id: NodeId) -> (ContextFeatures, Option) { + pub fn source_ids(&self) -> impl Iterator { + self.nodes.iter().filter_map(|(_, node)| match &node.construction_args { + ConstructionArgs::Value(value) => match &**value { + TaggedValue::SourceId(source) => Some(source), + _ => None, + }, + _ => None, + }) + } + + fn find_context_dependencies(&mut self, id: NodeId) -> (ContextModification, Option) { let mut branch_dependencies = Vec::new(); - let mut combined_deps = ContextFeatures::default(); + let mut combined_deps = ContextModification::default(); let node_index = id.0 as usize; let dependencies = self.nodes[node_index].1.context_dependencies; let mut inputs = match &self.nodes[node_index].1.construction_args { - // We pretend like we have already placed context modification nodes after ourselves because value nodes don't need to be cached - ConstructionArgs::Value(_) => return (dependencies.extract, Some(id)), ConstructionArgs::Nodes(items) => items.clone(), - ConstructionArgs::Inline(_) => return (dependencies.extract, Some(id)), + // We pretend like we have already placed context modification nodes after ourselves because value nodes don't need to be cached + ConstructionArgs::Value(value) if let TaggedValue::SourceId(source) = **value => { + return ( + ContextModification { + features: dependencies.extract, + sources: [source].into(), + }, + Some(id), + ); + } + ConstructionArgs::Value(_) | ConstructionArgs::Inline(_) => return (dependencies.extract.into(), Some(id)), }; // Compute the dependencies for each branch and combine all of them for &node in &inputs { let branch = self.find_context_dependencies(node); + combined_deps.features |= branch.0.features; + combined_deps.sources.extend(&branch.0.sources); branch_dependencies.push(branch); - combined_deps |= branch.0; } - let mut new_deps = combined_deps; - - // Remove requirements which this node provides - new_deps &= !dependencies.inject; - // Add requirements we have - new_deps |= dependencies.extract; + // Remove requirements which this node provides and add requirements we have + let features = (combined_deps.features & !dependencies.inject) | dependencies.extract; // If we either introduce new dependencies, we can cache all children which don't yet need that dependency - let we_introduce_new_deps = !combined_deps.contains(new_deps); + let we_introduces_new_deps = !combined_deps.features.contains(features); // For diverging branches, we can add a cache node for all branches which don't reqire all dependencies for (child_node, (deps, new_id)) in inputs.iter_mut().zip(branch_dependencies) { if let Some(new_id) = new_id { *child_node = new_id; - } else if we_introduce_new_deps || deps != combined_deps { + } else if we_introduces_new_deps || deps != combined_deps { *child_node = self.insert_context_nullification_node(*child_node, deps); } } @@ -410,15 +425,20 @@ impl ProtoNetwork { let net_injections = dependencies.inject.difference(dependencies.extract); // Which dependencies still need to be met after this node? - let remaining_deps_from_children = combined_deps.difference(net_injections); + let remaining_deps_from_children = combined_deps.features.difference(net_injections); // Do we satisfy any existing dependencies? - let we_supply_existing_deps = !combined_deps.difference(remaining_deps_from_children).is_empty(); + let we_supply_existing_deps = !combined_deps.features.difference(remaining_deps_from_children).is_empty(); + + let new_deps = ContextModification { + features, + sources: combined_deps.sources, + }; let mut new_id = None; if we_supply_existing_deps { // Our set of context dependencies has shrunk so we can add a cache node after the current node - new_id = Some(self.insert_context_nullification_node(id, new_deps)); + new_id = Some(self.insert_context_nullification_node(id, new_deps.clone())); } (new_deps, new_id) @@ -1063,6 +1083,89 @@ mod test { ); } + #[test] + fn retain_filter_placement_on_source_free_branch() { + let mut network = source_branch_network(&[SourceId(1)], &[]); + network.insert_context_nullification_nodes().unwrap(); + + let filters = nullification_filters(&network); + assert_eq!(filters.len(), 1); + let (filter_id, wrapped, retained) = &filters[0]; + assert_eq!(wrapped, "source_b"); + assert!(retained.is_empty()); + + let (source_a_id, _) = find_node(&network, "source_a"); + let (_, join) = find_node(&network, "join"); + let ConstructionArgs::Nodes(join_args) = &join.construction_args else { unreachable!() }; + assert_eq!(join_args, &vec![source_a_id, *filter_id]); + } + + #[test] + fn diverging_source_sets_filter_each_branch() { + let mut network = source_branch_network(&[SourceId(1)], &[SourceId(2)]); + network.insert_context_nullification_nodes().unwrap(); + + let mut filters = nullification_filters(&network); + filters.sort_by(|(_, a, _), (_, b, _)| a.cmp(b)); + let summary: Vec<_> = filters.iter().map(|(_, wrapped, retained)| (wrapped.as_str(), retained.as_slice())).collect(); + assert_eq!(summary, vec![("source_a", &[SourceId(1)][..]), ("source_b", &[SourceId(2)][..])]); + } + + #[test] + fn matching_source_sets_insert_no_filter() { + let mut network = source_branch_network(&[SourceId(1)], &[SourceId(1)]); + network.insert_context_nullification_nodes().unwrap(); + + assert!(nullification_filters(&network).is_empty()); + assert_eq!(network.source_ids().copied().collect::>(), vec![SourceId(1), SourceId(1)]); + } + + fn find_node<'a>(network: &'a ProtoNetwork, name: &str) -> (NodeId, &'a ProtoNode) { + network.nodes.iter().find(|(_, node)| node.identifier.as_str() == name).map(|(id, node)| (*id, node)).unwrap() + } + + fn nullification_filters(network: &ProtoNetwork) -> Vec<(NodeId, String, Vec)> { + let node = |id: NodeId| &network.nodes[id.0 as usize].1; + network + .nodes + .iter() + .filter(|(_, candidate)| candidate.identifier == graphene_core::context_modification::context_modification::IDENTIFIER) + .map(|(id, candidate)| { + let ConstructionArgs::Nodes(args) = &candidate.construction_args else { unreachable!() }; + let ConstructionArgs::Nodes(memoized) = &node(args[0]).construction_args else { unreachable!() }; + let ConstructionArgs::Value(value) = &node(args[1]).construction_args else { unreachable!() }; + let TaggedValue::ContextModification(modification) = &**value else { unreachable!() }; + (*id, node(memoized[0]).identifier.as_str().to_string(), modification.sources.iter().copied().collect()) + }) + .collect() + } + + fn source_branch_network(branch_a_sources: &[SourceId], branch_b_sources: &[SourceId]) -> ProtoNetwork { + let node = |name: &'static str, construction_args: ConstructionArgs| ProtoNode { + identifier: ProtoNodeIdentifier::new(name), + call_argument: concrete!(()), + construction_args, + ..Default::default() + }; + let mut nodes = vec![(NodeId(0), node("value", ConstructionArgs::Value(TaggedValue::Integer(2).into())))]; + let mut branch = |name: &'static str, sources: &[SourceId]| { + let mut inputs = vec![NodeId(0)]; + for &source in sources { + inputs.push(NodeId(nodes.len() as u64)); + nodes.push((NodeId(nodes.len() as u64), node("source_id", ConstructionArgs::Value(TaggedValue::SourceId(source).into())))); + } + let id = NodeId(nodes.len() as u64); + nodes.push((id, node(name, ConstructionArgs::Nodes(inputs)))); + id + }; + let branch_a = branch("source_a", branch_a_sources); + let branch_b = branch("source_b", branch_b_sources); + + let output = NodeId(nodes.len() as u64); + nodes.push((output, node("join", ConstructionArgs::Nodes(vec![branch_a, branch_b])))); + ProtoNetwork { inputs: vec![], output, nodes } + } + fn test_network() -> ProtoNetwork { ProtoNetwork { inputs: vec![NodeId(10)], diff --git a/node-graph/interpreted-executor/src/dynamic_executor/test.rs b/node-graph/interpreted-executor/src/dynamic_executor/test.rs index bdc6ba7a5f4..4dc309a72d0 100644 --- a/node-graph/interpreted-executor/src/dynamic_executor/test.rs +++ b/node-graph/interpreted-executor/src/dynamic_executor/test.rs @@ -259,7 +259,7 @@ fn nullification_chain_network(value: TaggedValue) -> ProtoNetwork { let mut memoize_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(0)]), vec![NodeId(1)]); memoize_node.identifier = graphene_core::memo::memoize::IDENTIFIER; - let features_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::ContextFeatures(Default::default()).into()), vec![NodeId(2)]); + let features_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::ContextModification(Default::default()).into()), vec![NodeId(2)]); let mut nullification_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(1), NodeId(2)]), vec![NodeId(3)]); nullification_node.identifier = graphene_core::context_modification::context_modification::IDENTIFIER; diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index e4ad4927d4a..1f4386f3dba 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -81,17 +81,17 @@ fn node_registry() -> HashMap, input: Context, fn_params: [Context => List]), // Context nullification #[cfg(feature = "gpu")] - async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item>, Context => Item]), - async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item, Context => Item]), - async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item, Context => Item]), - async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item, Context => Item]), - async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => ListDyn, Context => Item]), + async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item>, Context => Item]), + async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item, Context => Item]), + async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item, Context => Item]), + async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item, Context => Item]), + async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => ListDyn, Context => Item]), #[cfg(target_family = "wasm")] - async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item, Context => Item]), - async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item>, Context => Item]), - async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item, Context => Item]), - async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item>, Context => Item]), - async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item, Context => Item]), + async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item, Context => Item]), + async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item>, Context => Item]), + async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item, Context => Item]), + async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item>, Context => Item]), + async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item, Context => Item]), // ========== // MEMO NODES // ========== @@ -455,7 +455,7 @@ fn node_registry() -> HashMap = vec![ async_node!(identifier: graphene_core::memo::memoize::IDENTIFIER, graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => $value]), async_node!(identifier: graphene_core::memo::monitor::IDENTIFIER, graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => $value]), - async_node!(identifier: graphene_core::context_modification::context_modification::IDENTIFIER, graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => $value, Context => Item]), + async_node!(identifier: graphene_core::context_modification::context_modification::IDENTIFIER, graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => $value, Context => Item]), ]; entries }}; diff --git a/node-graph/libraries/core-types/src/context.rs b/node-graph/libraries/core-types/src/context.rs index 4aba3e2a6fa..2a573bdb505 100644 --- a/node-graph/libraries/core-types/src/context.rs +++ b/node-graph/libraries/core-types/src/context.rs @@ -4,6 +4,7 @@ use glam::DVec2; pub use no_std_types::context::{ArcCtx, Ctx}; use std::any::Any; use std::borrow::Borrow; +use std::collections::BTreeSet; use std::hash::{Hash, Hasher}; use std::panic::Location; use std::sync::Arc; @@ -132,6 +133,22 @@ impl ModifyPosition for T {} impl ModifyIndex for T {} impl ModifyVarArgs for T {} +// ==================== +// CONTEXT MODIFICATION +// ==================== + +#[derive(Debug, Clone, PartialEq, Eq, Default, graphene_hash::CacheHash, dyn_any::DynAny)] +pub struct ContextModification { + pub features: ContextFeatures, + pub sources: BTreeSet, +} + +impl From for ContextModification { + fn from(features: ContextFeatures) -> Self { + Self { features, sources: BTreeSet::new() } + } +} + // ================ // CONTEXT FEATURES // ================ @@ -626,6 +643,16 @@ impl OwnedContextImpl { } } + #[track_caller] + pub fn from_modification(value: T, modification: &ContextModification) -> Self { + let mut context = Self::from_flags(value, modification.features); + context.generations = context + .generations + .map(|generations| generations.iter().filter(|(source, _)| modification.sources.contains(source)).copied().collect::()) + .filter(|generations| !generations.is_empty()); + context + } + pub const fn empty() -> Self { OwnedContextImpl { parent: None, diff --git a/node-graph/libraries/core-types/src/registry.rs b/node-graph/libraries/core-types/src/registry.rs index 2e52c5bd0bf..5ee97ae38fa 100644 --- a/node-graph/libraries/core-types/src/registry.rs +++ b/node-graph/libraries/core-types/src/registry.rs @@ -60,6 +60,7 @@ pub enum RegistryValueSource { None, Default(&'static str), Scope(&'static str), + SourceId, } type NodeRegistry = LazyLock>>>; diff --git a/node-graph/libraries/core-types/src/runtime.rs b/node-graph/libraries/core-types/src/runtime.rs index ee1a6aa8eca..c16dab73a29 100644 --- a/node-graph/libraries/core-types/src/runtime.rs +++ b/node-graph/libraries/core-types/src/runtime.rs @@ -1,3 +1,5 @@ +use crate::uuid::NodeId; +use std::hash::{Hash, Hasher}; use std::pin::Pin; use std::sync::Arc; @@ -10,7 +12,15 @@ pub trait Spawn { fn spawn(&self, task: Task); } -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, graphene_hash::CacheHash)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, graphene_hash::CacheHash, dyn_any::DynAny)] pub struct SourceId(pub u64); +impl SourceId { + pub fn from_path(path: &[NodeId]) -> Self { + let mut hasher = rustc_hash::FxHasher::default(); + path.hash(&mut hasher); + Self(hasher.finish()) + } +} + pub type Generations = Arc<[(SourceId, u64)]>; diff --git a/node-graph/libraries/graphene-hash/src/lib.rs b/node-graph/libraries/graphene-hash/src/lib.rs index 4476ad4fed0..2e0a84dd9ea 100644 --- a/node-graph/libraries/graphene-hash/src/lib.rs +++ b/node-graph/libraries/graphene-hash/src/lib.rs @@ -194,6 +194,17 @@ impl CacheHash for Vec { } } +#[cfg(feature = "std")] +impl CacheHash for std::collections::BTreeSet { + #[inline] + fn cache_hash(&self, state: &mut H) { + core::hash::Hash::hash(&self.len(), state); + for item in self { + item.cache_hash(state); + } + } +} + #[cfg(feature = "std")] impl CacheHash for Box { #[inline] diff --git a/node-graph/nodes/gcore/src/context_modification.rs b/node-graph/nodes/gcore/src/context_modification.rs index 52b5ceb540d..0e759dd955b 100644 --- a/node-graph/nodes/gcore/src/context_modification.rs +++ b/node-graph/nodes/gcore/src/context_modification.rs @@ -1,5 +1,5 @@ use core::f64; -use core_types::context::{CloneVarArgs, Context, ContextFeatures, Ctx, ExtractAll}; +use core_types::context::{CloneVarArgs, Context, ContextModification, Ctx, ExtractAll}; use core_types::list::{AttributeValueDyn, Item, List, ListDyn, NodeIdPath}; use core_types::transform::Footprint; use core_types::{Color, OwnedContextImpl}; @@ -45,9 +45,9 @@ async fn context_modification( )] value: impl Node, Output = T>, /// The parts of the context to keep when evaluating the input value. All other parts are nullified. - features_to_keep: Item, + modification: Item, ) -> T { - let new_context = OwnedContextImpl::from_flags(ctx, features_to_keep.into_element()); + let new_context = OwnedContextImpl::from_modification(ctx, modification.element()); value.eval(Some(new_context.into())).await } @@ -55,6 +55,7 @@ async fn context_modification( #[cfg(test)] mod tests { use super::*; + use core_types::context::ContextFeatures; use core_types::graphene_hash::CacheHash; use core_types::transform::Footprint; use std::collections::hash_map::DefaultHasher; diff --git a/node-graph/preprocessor/src/lib.rs b/node-graph/preprocessor/src/lib.rs index a7a91c4da27..55f72ef9cf6 100644 --- a/node-graph/preprocessor/src/lib.rs +++ b/node-graph/preprocessor/src/lib.rs @@ -351,6 +351,7 @@ pub fn node_inputs(fields: &[registry::FieldMetadata], first_node_io: &NodeIOTyp } } RegistryValueSource::Scope(data) => return NodeInput::scope(*data), + RegistryValueSource::SourceId => return NodeInput::Reflection(graph_craft::document::DocumentNodeMetadata::SourceId), }; // A ranked `Item` type prefers a bare `T` value (promoted at resolution), since bare values drive the Properties panel widgets From 6e5b814a0b40587cc1779fa2c772cd6800b8f123 Mon Sep 17 00:00:00 2001 From: Timon Date: Fri, 9 Oct 2026 18:58:21 +0000 Subject: [PATCH 21/54] Add async runtime and memo table --- node-graph/libraries/core-types/src/lib.rs | 2 +- .../libraries/core-types/src/runtime.rs | 261 +++++++++++++++++- 2 files changed, 261 insertions(+), 2 deletions(-) diff --git a/node-graph/libraries/core-types/src/lib.rs b/node-graph/libraries/core-types/src/lib.rs index c1de01d88f0..3b723fd3e10 100644 --- a/node-graph/libraries/core-types/src/lib.rs +++ b/node-graph/libraries/core-types/src/lib.rs @@ -41,7 +41,7 @@ pub use no_std_types::choice_type; pub use no_std_types::color; pub use no_std_types::shaders; pub use num_traits; -pub use runtime::{Generations, SourceId}; +pub use runtime::{Generations, RuntimeHandle, SourceId}; use std::any::TypeId; use std::future::Future; use std::pin::Pin; diff --git a/node-graph/libraries/core-types/src/runtime.rs b/node-graph/libraries/core-types/src/runtime.rs index c16dab73a29..fa4097b4b2d 100644 --- a/node-graph/libraries/core-types/src/runtime.rs +++ b/node-graph/libraries/core-types/src/runtime.rs @@ -1,7 +1,12 @@ +use crate::poll::Poll; use crate::uuid::NodeId; +use dyn_any::WasmNotSend; +use std::collections::HashMap; +use std::future::Future; use std::hash::{Hash, Hasher}; use std::pin::Pin; -use std::sync::Arc; +use std::sync::{Arc, Mutex, PoisonError}; +use std::task::{Context, Waker}; #[cfg(not(target_family = "wasm"))] pub type Task = Pin + Send + 'static>>; @@ -24,3 +29,257 @@ impl SourceId { } pub type Generations = Arc<[(SourceId, u64)]>; + +pub trait Runtime { + fn spawn(&self, source: SourceId, task: Task); +} + +#[derive(Clone, dyn_any::DynAny)] +pub struct RuntimeHandle(Arc); + +impl Runtime for RuntimeHandle { + fn spawn(&self, source: SourceId, task: Task) { + self.0.spawn(source, task); + } +} + +impl std::fmt::Debug for RuntimeHandle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RuntimeHandle").finish_non_exhaustive() + } +} + +impl PartialEq for RuntimeHandle { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.0, &other.0) + } +} + +impl graphene_hash::CacheHash for RuntimeHandle { + fn cache_hash(&self, _state: &mut H) {} +} + +pub struct AsyncRuntime(Arc); + +impl AsyncRuntime { + pub fn new(spawner: Arc) -> Self { + Self(Arc::new(RuntimeInner { + generations: Mutex::default(), + spawner, + })) + } + + pub fn retain_sources(&self, live: &[SourceId]) { + let mut generations = self.0.generations.lock().unwrap_or_else(PoisonError::into_inner); + generations.retain(|source, _| live.contains(source)); + for source in live { + generations.entry(*source).or_insert(0); + } + } + + pub fn snapshot(&self) -> Generations { + let generations = self.0.generations.lock().unwrap_or_else(PoisonError::into_inner); + let mut snapshot: Vec<_> = generations.iter().map(|(source, generation)| (*source, *generation)).collect(); + snapshot.sort_unstable(); + snapshot.into() + } + + pub fn handle(&self) -> RuntimeHandle { + RuntimeHandle(Arc::clone(&self.0)) + } +} + +impl Runtime for AsyncRuntime { + fn spawn(&self, source: SourceId, task: Task) { + self.0.spawn(source, task); + } +} + +struct RuntimeInner { + generations: Mutex>, + spawner: Arc, +} + +impl RuntimeInner { + fn spawn(self: &Arc, source: SourceId, task: Task) { + let inner = Arc::clone(self); + self.spawner.spawn(Box::pin(async move { + task.await; + if let Some(generation) = inner.generations.lock().unwrap_or_else(PoisonError::into_inner).get_mut(&source) { + *generation += 1; + } + })); + } +} + +pub struct MemoTable(Arc>>>>); + +impl Default for MemoTable { + fn default() -> Self { + Self(Arc::default()) + } +} + +impl Clone for MemoTable { + fn clone(&self) -> Self { + Self(Arc::clone(&self.0)) + } +} + +impl std::fmt::Debug for MemoTable { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("MemoTable").finish_non_exhaustive() + } +} + +impl MemoTable { + pub fn resolve(&self, key: u64, runtime: &dyn Runtime, source: SourceId, placeholder: Option, future: impl FnOnce() -> F) -> Poll + where + F: Future> + WasmNotSend + 'static, + { + { + let mut table = self.0.lock().unwrap_or_else(PoisonError::into_inner); + match table.get(&key) { + Some(Some(landed)) => return landed.clone(), + Some(None) => return placeholder.map_or(Poll::Pending, Poll::Partial), + None => table.insert(key, None), + }; + } + + // Fast path for futures that are ready immediately + // TODO(Timon): check if this would ever cause a problem; running a future on the graph executor thread might also be a bit wierd + let mut future = Box::pin(future()); + if let std::task::Poll::Ready(state) = future.as_mut().poll(&mut Context::from_waker(Waker::noop())) { + self.0.lock().unwrap_or_else(PoisonError::into_inner).insert(key, Some(state.clone())); + return state; + } + + let slots = Arc::clone(&self.0); + runtime.spawn( + source, + Box::pin(async move { + let state = future.await; + slots.lock().unwrap_or_else(PoisonError::into_inner).insert(key, Some(state)); + }), + ); + match self.0.lock().unwrap_or_else(PoisonError::into_inner).get(&key) { + Some(Some(resolved)) => resolved.clone(), + _ => placeholder.map_or(Poll::Pending, Poll::Partial), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn poll_once(task: &mut Task) -> bool { + task.as_mut().poll(&mut Context::from_waker(Waker::noop())).is_ready() + } + + #[derive(Default)] + struct CollectSpawner(Mutex>); + + impl Spawn for CollectSpawner { + fn spawn(&self, task: Task) { + self.0.lock().unwrap().push(task); + } + } + + impl CollectSpawner { + fn drain(&self) -> usize { + let tasks = std::mem::take(&mut *self.0.lock().unwrap()); + let count = tasks.len(); + for mut task in tasks { + while !poll_once(&mut task) {} + } + count + } + } + + struct BlockingSpawner; + + impl Spawn for BlockingSpawner { + fn spawn(&self, mut task: Task) { + while !poll_once(&mut task) {} + } + } + + struct YieldOnce(bool); + + impl Future for YieldOnce { + type Output = (); + fn poll(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> std::task::Poll<()> { + if self.0 { + return std::task::Poll::Ready(()); + } + self.0 = true; + context.waker().wake_by_ref(); + std::task::Poll::Pending + } + } + + const SOURCE: SourceId = SourceId(7); + + #[test] + fn a_completion_bumps_its_generation() { + let spawner = Arc::new(CollectSpawner::default()); + let runtime = AsyncRuntime::new(spawner.clone()); + runtime.retain_sources(&[SOURCE]); + + runtime.handle().spawn(SOURCE, Box::pin(YieldOnce(false))); + assert_eq!(&*runtime.snapshot(), &[(SOURCE, 0)]); + + assert_eq!(spawner.drain(), 1); + assert_eq!(&*runtime.snapshot(), &[(SOURCE, 1)]); + } + + #[test] + fn an_inline_completion_and_a_dropped_source_bump_nothing() { + let spawner = Arc::new(CollectSpawner::default()); + let runtime = AsyncRuntime::new(spawner.clone()); + runtime.retain_sources(&[SOURCE]); + + let slots = MemoTable::default(); + assert_eq!(slots.resolve(1, &runtime, SOURCE, None, || async { Poll::Final(1u32) }), Poll::Final(1)); + assert_eq!(spawner.drain(), 0); + assert_eq!(&*runtime.snapshot(), &[(SOURCE, 0)]); + + runtime.spawn(SOURCE, Box::pin(YieldOnce(false))); + runtime.retain_sources(&[]); + assert_eq!(spawner.drain(), 1); + assert!(runtime.snapshot().is_empty()); + } + + #[test] + fn a_slot_serves_the_placeholder_until_the_response_lands() { + let spawner = Arc::new(CollectSpawner::default()); + let runtime = AsyncRuntime::new(spawner.clone()); + runtime.retain_sources(&[SOURCE]); + let slots = MemoTable::default(); + let fetch = || async { + YieldOnce(false).await; + Poll::Final(42u32) + }; + + assert_eq!(slots.resolve(1, &runtime, SOURCE, Some(0), fetch), Poll::Partial(0)); + assert_eq!(slots.resolve(1, &runtime, SOURCE, None, fetch), Poll::Pending); + assert_eq!(spawner.drain(), 1, "an in-flight slot is not spawned twice"); + assert_eq!(slots.resolve(1, &runtime, SOURCE, Some(0), fetch), Poll::Final(42)); + assert_eq!(slots.resolve(2, &runtime, SOURCE, None, || async { Poll::Final(1u32) }), Poll::Final(1)); + } + + #[test] + fn a_blocking_spawner_lands_the_slot_in_the_same_resolve() { + let runtime = AsyncRuntime::new(Arc::new(BlockingSpawner)); + runtime.retain_sources(&[SOURCE]); + let slots = MemoTable::default(); + let fetch = || async { + YieldOnce(false).await; + Poll::Final(42u32) + }; + + assert_eq!(slots.resolve(1, &runtime, SOURCE, Some(0), fetch), Poll::Final(42)); + assert_eq!(&*runtime.snapshot(), &[(SOURCE, 1)]); + } +} From bfe51f51563fa7d3b9cf6dc2a65755338819af86 Mon Sep 17 00:00:00 2001 From: Timon Date: Fri, 9 Oct 2026 18:59:50 +0000 Subject: [PATCH 22/54] Pass runtime through scope, expose source ids --- node-graph/graph-craft/src/document/value.rs | 9 ++++++++- .../interpreted-executor/src/dynamic_executor.rs | 10 ++++++++++ node-graph/interpreted-executor/src/util.rs | 13 +++++++++++-- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index dd821afa37b..52281b906d9 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -7,7 +7,7 @@ use core_types::color::SRGBA8; use core_types::list::{Item, List, NodeIdPath}; use core_types::transfer_curve::TransferCurve; use core_types::transform::Footprint; -use core_types::{CacheHash, Color, ContextModification, MemoHash, Node, SourceId, Type, TypeDescriptor}; +use core_types::{CacheHash, Color, ContextModification, MemoHash, Node, RuntimeHandle, SourceId, Type, TypeDescriptor}; use dyn_any::DynAny; pub use dyn_any::StaticType; pub use glam::{DAffine2, DVec2, IVec2, UVec2}; @@ -118,6 +118,8 @@ macro_rules! tagged_value { #[serde(skip)] SourceId(SourceId), #[serde(skip)] + Runtime(RuntimeHandle), + #[serde(skip)] EditorApi(Arc), /// Only used by the `resource` node, should never be serialized #[serde(skip)] @@ -151,6 +153,7 @@ macro_rules! tagged_value { Self::DocumentNode(node) => node.cache_hash(state), Self::ContextModification(modification) => modification.cache_hash(state), Self::SourceId(source) => source.cache_hash(state), + Self::Runtime(runtime) => runtime.cache_hash(state), Self::RenderOutput(x) => x.cache_hash(state), Self::EditorApi(x) => x.cache_hash(state), Self::ResourceHash(x) => x.cache_hash(state), @@ -225,6 +228,7 @@ macro_rules! tagged_value { Self::DocumentNode(node) => Box::new(node), Self::ContextModification(modification) => Box::new(Item::new_from_element(modification)), Self::SourceId(source) => Box::new(Item::new_from_element(source)), + Self::Runtime(runtime) => Box::new(Item::new_from_element(runtime)), Self::EditorApi(x) => Box::new(Item::new_from_element(x)), Self::ResourceHash(x) => Box::new(Item::new_from_element(x)), } @@ -294,6 +298,7 @@ macro_rules! tagged_value { Self::DocumentNode(node) => Arc::new(node), Self::ContextModification(modification) => Arc::new(Item::new_from_element(modification)), Self::SourceId(source) => Arc::new(Item::new_from_element(source)), + Self::Runtime(runtime) => Arc::new(Item::new_from_element(runtime)), Self::EditorApi(x) => Arc::new(Item::new_from_element(x)), Self::ResourceHash(x) => Arc::new(Item::new_from_element(x)), } @@ -326,6 +331,7 @@ macro_rules! tagged_value { Self::DocumentNode(_) => concrete!(DocumentNode), Self::ContextModification(_) => item!(ContextModification), Self::SourceId(_) => item!(SourceId), + Self::Runtime(_) => item!(RuntimeHandle), Self::EditorApi(_) => item!(Arc), Self::ResourceHash(_) => item!(ResourceHash), }; @@ -493,6 +499,7 @@ macro_rules! tagged_value { Self::DocumentNode(node) => format!("DocumentNode({node:?})"), Self::ContextModification(modification) => format!("ContextModification({modification:?})"), Self::SourceId(source) => format!("SourceId({source:?})"), + Self::Runtime(_) => "Runtime".to_string(), Self::EditorApi(_) => "PlatformEditorApi".to_string(), Self::ResourceHash(hash) => format!("ResourceHash({hash:?})"), } diff --git a/node-graph/interpreted-executor/src/dynamic_executor.rs b/node-graph/interpreted-executor/src/dynamic_executor.rs index 5b9205a886a..2cb28b36bda 100644 --- a/node-graph/interpreted-executor/src/dynamic_executor.rs +++ b/node-graph/interpreted-executor/src/dynamic_executor.rs @@ -1,4 +1,5 @@ use crate::node_registry; +use core_types::SourceId; use dyn_any::StaticType; use graph_craft::Type; use graph_craft::document::NodeId; @@ -20,6 +21,7 @@ pub struct DynamicExecutor { typing_context: TypingContext, // This allows us to keep the nodes around for one more frame which is used for introspection orphaned_nodes: HashSet, + sources: Vec, } impl Default for DynamicExecutor { @@ -29,6 +31,7 @@ impl Default for DynamicExecutor { tree: Default::default(), typing_context: TypingContext::new(&node_registry::NODE_REGISTRY), orphaned_nodes: HashSet::new(), + sources: Vec::new(), } } } @@ -52,6 +55,7 @@ impl DynamicExecutor { let mut typing_context = TypingContext::new(&node_registry::NODE_REGISTRY); typing_context.update(&proto_network)?; let output = proto_network.output; + let sources = proto_network.source_ids().copied().collect(); let tree = BorrowTree::new(proto_network, &typing_context)?; Ok(Self { @@ -59,9 +63,14 @@ impl DynamicExecutor { output, typing_context, orphaned_nodes: HashSet::new(), + sources, }) } + pub fn sources(&self) -> &[SourceId] { + &self.sources + } + /// 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 { @@ -87,6 +96,7 @@ impl DynamicExecutor { (ResolvedDocumentNodeTypesDelta { add, remove: Vec::new() }, e) })?; + self.sources = proto_network.source_ids().copied().collect(); let (add, orphaned) = self.tree.update(proto_network, &self.typing_context).map_err(|e| (ResolvedDocumentNodeTypesDelta::default(), e))?; let old_to_remove = core::mem::replace(&mut self.orphaned_nodes, orphaned); let mut remove = Vec::with_capacity(old_to_remove.len() - self.orphaned_nodes.len().min(old_to_remove.len())); diff --git a/node-graph/interpreted-executor/src/util.rs b/node-graph/interpreted-executor/src/util.rs index 3cc374d495f..b8b1c720f4b 100644 --- a/node-graph/interpreted-executor/src/util.rs +++ b/node-graph/interpreted-executor/src/util.rs @@ -5,10 +5,11 @@ use graph_craft::generic; use graph_craft::{concrete, item}; use graphene_std::Context; use graphene_std::ContextFeatures; +use graphene_std::RuntimeHandle; use graphene_std::uuid::NodeId; use std::sync::Arc; -pub fn wrap_network_in_scope(network: NodeNetwork, editor_api: Arc) -> NodeNetwork { +pub fn wrap_network_in_scope(network: NodeNetwork, editor_api: Arc, runtime: RuntimeHandle) -> NodeNetwork { let inner_network = DocumentNode { implementation: DocumentNodeImplementation::Network(network), inputs: vec![], @@ -113,8 +114,16 @@ pub fn wrap_network_in_scope(network: NodeNetwork, editor_api: Arc))), + ("runtime".to_string(), (NodeId(3), item!(RuntimeHandle))), ]; - let scope_injections = vec![("editor-api".to_string(), (NodeId(2), item!(Arc)))]; NodeNetwork { exports: vec![NodeInput::node(NodeId(1), 0)], From 436b41cf8644af5fbbbd9f340888543b1ebc4c29 Mon Sep 17 00:00:00 2001 From: Timon Date: Fri, 9 Oct 2026 19:01:06 +0000 Subject: [PATCH 23/54] Set up async runtime in editor, CLI and benches --- editor/src/application.rs | 7 +++-- editor/src/node_graph_executor/runtime.rs | 31 ++++++++++++++++--- node-graph/graphene-cli/src/export.rs | 8 +++-- node-graph/graphene-cli/src/main.rs | 21 ++++++++++--- .../benches/benchmark_util.rs | 9 +++++- 5 files changed, 61 insertions(+), 15 deletions(-) diff --git a/editor/src/application.rs b/editor/src/application.rs index 643c2aec39f..7eb2e23dbb7 100644 --- a/editor/src/application.rs +++ b/editor/src/application.rs @@ -8,7 +8,6 @@ use std::sync::{Arc, OnceLock}; pub struct Editor { pub dispatcher: Dispatcher, - task_executor: TaskExecutor, } impl Editor { @@ -28,8 +27,9 @@ impl Editor { dispatcher.message_handlers.future_message_handler = FutureMessageHandler::new(task_executor.handle(), wake); application_io.inject_resource_proxy(dispatcher.message_handlers.resource_storage_message_handler.resources()); crate::node_graph_executor::replace_application_io(application_io); + crate::node_graph_executor::replace_task_executor(task_executor.handle()); - Self { dispatcher, task_executor } + Self { dispatcher } } #[cfg(test)] @@ -41,11 +41,12 @@ impl Editor { let task_executor = TaskExecutor::blocking(); let mut dispatcher = Dispatcher::with_executor(executor); dispatcher.message_handlers.future_message_handler = FutureMessageHandler::new(task_executor.handle(), Arc::new(|| {})); - let editor = Self { dispatcher, task_executor }; + let editor = Self { dispatcher }; let mut application_io = PlatformApplicationIo::default(); application_io.inject_resource_proxy(editor.dispatcher.message_handlers.resource_storage_message_handler.resources()); runtime.replace_application_io(application_io); + runtime.replace_task_executor(task_executor.handle()); (editor, runtime) } diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index d03291f7ef6..14107f16304 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -1,5 +1,6 @@ use super::*; use crate::messages::frontend::utility_types::{ExportBounds, FileType}; +use crate::task::TaskExecutorHandle; use glam::{DAffine2, DVec2, UVec2}; use graph_craft::application_io::resource::ResourceRegistry; use graph_craft::application_io::{PlatformApplicationIo, PlatformEditorApi}; @@ -17,6 +18,7 @@ use graphene_std::ops::ConvertAsync; use graphene_std::platform_application_io::canvas_utils::{Canvas, CanvasSurface, CanvasSurfaceHandle}; use graphene_std::raster_types::Raster; use graphene_std::renderer::{Render, RenderParams, RenderSvgSegmentList, SvgRender, SvgSegment}; +use graphene_std::runtime::AsyncRuntime; use graphene_std::transform::RenderQuality; use graphene_std::vector::Vector; use graphene_std::vector::style::RenderMode; @@ -40,6 +42,7 @@ pub struct NodeRuntime { editor_preferences: EditorPreferences, old_graph: Option, update_thumbnails: bool, + async_runtime: Option, editor_api: Arc, resources: ResourceRegistry, @@ -106,6 +109,7 @@ impl NodeRuntime { old_graph: None, resources: ResourceRegistry::default(), update_thumbnails: true, + async_runtime: None, editor_api: PlatformEditorApi { editor_preferences: Box::new(EditorPreferences::default()), @@ -324,7 +328,7 @@ impl NodeRuntime { } fn update_network(&mut self, graph: NodeNetwork) -> Result { - let mut scoped_network = wrap_network_in_scope(graph, self.editor_api.clone()); + let mut scoped_network = wrap_network_in_scope(graph, self.editor_api.clone(), self.async_runtime().handle()); if let Err(e) = self.preprocessor.preprocess(&mut scoped_network, &|resource_id| self.resources.hash(&resource_id)) { return Err((ResolvedDocumentNodeTypesDelta::default(), e.to_string())); @@ -346,17 +350,22 @@ impl NodeRuntime { .collect::>(); assert_ne!(proto_network.nodes.len(), 0, "No proto nodes exist?"); - self.executor.update(proto_network).map_err(|(types, e)| { + let result = self.executor.update(proto_network).map_err(|(types, e)| { self.node_graph_errors.clone_from(&e); (types, format!("{e:?}")) - }) + }); + self.async_runtime().retain_sources(self.executor.sources()); + result } async fn execute_network(&mut self, render_config: RenderConfig) -> Result { use graph_craft::graphene_compiler::Executor; match self.executor.input_type() { - Some(t) if t == concrete!(Context) => (&self.executor).execute(render_config.into_context()).await.map_err(|e| e.to_string()), + Some(t) if t == concrete!(Context) => (&self.executor) + .execute(render_config.into_context_with_generations(self.async_runtime().snapshot())) + .await + .map_err(|e| e.to_string()), Some(t) if t == concrete!(()) => (&self.executor).execute(()).await.map_err(|e| e.to_string()), Some(t) => Err(format!("Invalid input type {t:?}")), _ => Err(format!("No input type:\n{:?}", self.node_graph_errors)), @@ -570,6 +579,12 @@ pub(crate) fn replace_application_io(application_io: PlatformApplicationIo) { node_runtime.replace_application_io(application_io); } } +pub(crate) fn replace_task_executor(handle: TaskExecutorHandle) { + let mut node_runtime = NODE_RUNTIME.lock(); + if let Some(node_runtime) = &mut *node_runtime { + node_runtime.replace_task_executor(handle); + } +} impl NodeRuntime { pub(crate) fn replace_application_io(&mut self, application_io: PlatformApplicationIo) { @@ -579,6 +594,14 @@ impl NodeRuntime { } .into(); } + + pub(crate) fn replace_task_executor(&mut self, handle: TaskExecutorHandle) { + self.async_runtime = Some(AsyncRuntime::new(Arc::new(handle))); + } + + fn async_runtime(&self) -> &AsyncRuntime { + self.async_runtime.as_ref().expect("the task executor is installed before the first graph update") + } } /// Which node is inspected and which monitor node is used (if any) for the current execution diff --git a/node-graph/graphene-cli/src/export.rs b/node-graph/graphene-cli/src/export.rs index 602f5cf2d1e..83d2392e46b 100644 --- a/node-graph/graphene-cli/src/export.rs +++ b/node-graph/graphene-cli/src/export.rs @@ -1,5 +1,6 @@ use graph_craft::document::value::{RenderOutputType, TaggedValue, UVec2}; use graph_craft::graphene_compiler::Executor; +use graphene_std::Generations; use graphene_std::application_io::{ExportFormat, RenderConfig, TimingInformation}; use graphene_std::core_types::ops::ConvertAsync; use graphene_std::core_types::transform::Footprint; @@ -28,6 +29,7 @@ pub fn detect_file_type(path: &Path) -> Result { } } +#[allow(clippy::too_many_arguments)] pub async fn export_document( executor: &DynamicExecutor, wgpu_executor: &wgpu_executor::WgpuExecutor, @@ -36,6 +38,7 @@ pub async fn export_document( scale: f64, (width, height): (Option, Option), transparent: bool, + generations: Generations, ) -> Result<(), Box> { // Determine export format based on file type let export_format = match file_type { @@ -57,7 +60,7 @@ pub async fn export_document( } // Execute the graph - let result = executor.execute(render_config.into_context()).await?; + let result = executor.execute(render_config.into_context_with_generations(generations)).await?; // Handle the result based on output type match result { @@ -156,6 +159,7 @@ pub async fn export_gif( scale: f64, (width, height): (Option, Option), animation: AnimationParams, + generations: Generations, ) -> Result<(), Box> { use image::codecs::gif::{GifEncoder, Repeat}; use image::{Frame, RgbaImage}; @@ -195,7 +199,7 @@ pub async fn export_gif( } // Execute the graph for this frame - let result = executor.execute(render_config.into_context()).await?; + let result = executor.execute(render_config.into_context_with_generations(generations.clone())).await?; // Extract RGBA data from result let (data, img_width, img_height) = match result { diff --git a/node-graph/graphene-cli/src/main.rs b/node-graph/graphene-cli/src/main.rs index f403f501c3c..2fdbcdc772f 100644 --- a/node-graph/graphene-cli/src/main.rs +++ b/node-graph/graphene-cli/src/main.rs @@ -12,6 +12,7 @@ use graph_craft::graphene_compiler::Compiler; use graph_craft::proto::ProtoNetwork; use graph_craft::util::load_network; use graphene_std::application_io::ApplicationIo; +use graphene_std::runtime::{AsyncRuntime, RuntimeHandle, Spawn, Task}; use interpreted_executor::dynamic_executor::DynamicExecutor; use interpreted_executor::util::wrap_network_in_scope; use std::error::Error; @@ -176,7 +177,8 @@ async fn main() -> Result<(), Box> { application_io: Some(application_io_for_api), editor_preferences: Box::new(preferences), }); - let proto_graph = compile_graph(node_network, editor_api, gdd.as_ref())?; + let runtime = AsyncRuntime::new(Arc::new(TokioSpawner)); + let proto_graph = compile_graph(node_network, editor_api, gdd.as_ref(), runtime.handle())?; match app.command { Command::Compile { print_proto, .. } => { @@ -208,6 +210,8 @@ async fn main() -> Result<(), Box> { // Create executor let executor = create_executor(proto_graph)?; + runtime.retain_sources(executor.sources()); + let generations = runtime.snapshot(); if fps <= 0. { return Err("Fps number must be positive".into()); @@ -216,9 +220,9 @@ async fn main() -> Result<(), Box> { // Perform export based on file type if file_type == export::FileType::Gif { let animation = export::AnimationParams::new(fps, frames, duration); - export::export_gif(&executor, wgpu_executor_ref, output, scale, (width, height), animation).await?; + export::export_gif(&executor, wgpu_executor_ref, output, scale, (width, height), animation, generations).await?; } else { - export::export_document(&executor, wgpu_executor_ref, output, file_type, scale, (width, height), transparent).await?; + export::export_document(&executor, wgpu_executor_ref, output, file_type, scale, (width, height), transparent, generations).await?; } } _ => unreachable!("All other commands should be handled before this match statement is run"), @@ -256,10 +260,10 @@ fn init_logging(log_level: u8) { .unwrap(); } -fn compile_graph(network: NodeNetwork, editor_api: Arc, gdd: Option<&GddV1>) -> Result> { +fn compile_graph(network: NodeNetwork, editor_api: Arc, gdd: Option<&GddV1>, runtime: RuntimeHandle) -> Result> { let preprocessor = preprocessor::Preprocessor::new(); - let mut network = wrap_network_in_scope(network, editor_api); + let mut network = wrap_network_in_scope(network, editor_api, runtime); // A `.gdd` resolves resource hashes from its registry; a legacy `.graphite` has no resource store, so it // preprocesses against an empty registry (matching the pre-`.gdd` CLI behavior). @@ -278,3 +282,10 @@ fn create_executor(proto_network: ProtoNetwork) -> Result (DynamicExecutor, ProtoNetwork) { let network = load_from_name(name); let editor_api = std::sync::Arc::new(EditorApi::default()); - let mut network = wrap_network_in_scope(network, editor_api); + let mut network = wrap_network_in_scope(network, editor_api, AsyncRuntime::new(std::sync::Arc::new(DummySpawner)).handle()); let preprocessor = preprocessor::Preprocessor::new(); preprocessor.preprocess(&mut network, &|_| None).unwrap(); let proto_network = compile(network); @@ -26,3 +27,9 @@ where f(name, group); } } + +// TODO: Consider replacing this with a real spawner and benchmarking async nodes +struct DummySpawner; +impl Spawn for DummySpawner { + fn spawn(&self, _task: Task) {} +} From 208d541154168bef1690b26e31bfc84d8b0fee54 Mon Sep 17 00:00:00 2001 From: Timon Date: Fri, 9 Oct 2026 19:22:48 +0000 Subject: [PATCH 24/54] Give async nodes runtime and source id inputs --- .../interpreted-executor/src/node_registry.rs | 4 ++ .../libraries/core-types/src/registry.rs | 1 + node-graph/node-macro/src/codegen.rs | 49 ++++++++++++------- node-graph/node-macro/src/parsing.rs | 37 +++++++++++++- node-graph/preprocessor/src/lib.rs | 10 +++- 5 files changed, 81 insertions(+), 20 deletions(-) diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index 1f4386f3dba..7708b632537 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -89,6 +89,8 @@ fn node_registry() -> HashMap, input: Context, fn_params: [Context => Item, Context => Item]), async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item>, Context => Item]), + async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item, Context => Item]), + async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item, Context => Item]), async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item, Context => Item]), async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item>, Context => Item]), async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item, Context => Item]), @@ -137,6 +139,8 @@ fn node_registry() -> HashMap, input: Context, fn_params: [Context => Item]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item>]), + async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item]), + async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item]), #[cfg(feature = "gpu")] async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List>]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List]), diff --git a/node-graph/libraries/core-types/src/registry.rs b/node-graph/libraries/core-types/src/registry.rs index 5ee97ae38fa..3f81c5016ae 100644 --- a/node-graph/libraries/core-types/src/registry.rs +++ b/node-graph/libraries/core-types/src/registry.rs @@ -17,6 +17,7 @@ pub struct NodeMetadata { pub context_features: Vec, pub memoize: bool, pub inject_scope: bool, + pub asynchronous: bool, } // Translation struct between macro and definition diff --git a/node-graph/node-macro/src/codegen.rs b/node-graph/node-macro/src/codegen.rs index 54c317d772d..abf80d6ce46 100644 --- a/node-graph/node-macro/src/codegen.rs +++ b/node-graph/node-macro/src/codegen.rs @@ -41,8 +41,21 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn }; let struct_name = format_ident!("{}Node", struct_name); + let async_hidden_fields = is_async.then(|| { + [ + ParsedField::hidden( + "_runtime", + parse_quote!(#core_types::list::Item<#core_types::RuntimeHandle>), + ParsedValueSource::Scope(Box::new(parse_quote!("runtime"))), + ), + ParsedField::hidden("_source", parse_quote!(#core_types::list::Item<#core_types::SourceId>), ParsedValueSource::SourceId), + ] + }); + + let fields_iter = fields.iter().chain(async_hidden_fields.iter().flatten()); + // Separate data fields from regular fields - let (data_fields, regular_fields): (Vec<_>, Vec<_>) = fields.iter().partition(|f| f.is_data_field); + let (data_fields, regular_fields): (Vec<_>, Vec<_>) = fields_iter.partition(|f| f.is_data_field); // The primary's position among the regular fields, which `#[scope]` fields may precede (`#[data]` fields are already partitioned out) let primary_regular_index = regular_fields.iter().position(|field| !field.is_environment()); @@ -195,6 +208,7 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn quote!(RegistryValueSource::Scope(#data.as_static_str())) } } + ParsedValueSource::SourceId => quote!(RegistryValueSource::SourceId), _ => quote!(RegistryValueSource::None), }, None => quote!(RegistryValueSource::None), @@ -688,11 +702,12 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let mapped_struct_name = format_ident!("{}Mapped", struct_name); let list_content_struct_name = format_ident!("{}ListContent", struct_name); - let register_node_impl = generate_register_node_impl(parsed, &field_names, &struct_name, &mapped_struct_name, &list_content_struct_name, &identifier)?; + let register_node_impl = generate_register_node_impl(parsed, ®ular_fields, &field_names, &struct_name, &mapped_struct_name, &list_content_struct_name, &identifier)?; let import_name = format_ident!("_IMPORT_STUB_{}", mod_name.to_string().to_case(Case::UpperSnake)); let properties = &attributes.properties_string.as_ref().map(|value| quote!(Some(#value))).unwrap_or(quote!(None)); let memoize_flag = attributes.memoize; + let asynchronous_flag = parsed.is_async; let inject_scope_flag = attributes.inject_scope; let cfg = crate::shader_nodes::modify_cfg(attributes); @@ -908,6 +923,7 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn context_features: vec![#(ContextFeature::#context_features,)*], memoize: #memoize_flag, inject_scope: #inject_scope_flag, + asynchronous: #asynchronous_flag, fields: vec![ #( FieldMetadata { @@ -1052,6 +1068,9 @@ fn primary_item_element(parsed: &ParsedNodeFn) -> Option { /// Whether the element-wise node gets a mapped `List` wire variant: an eager primary is its own frame source, while a lazy primary (or a `()` generator) draws the frame from its ranked eager params, so it needs at least one. fn generates_mapped_variant(parsed: &ParsedNodeFn) -> bool { + if parsed.is_async { + return false; + } // A `()` generator frames over its ranked params exactly like a lazy primary, but generating fresh content per slot instead of transforming an upstream item if is_generator_frame(parsed) { return true; @@ -1099,6 +1118,7 @@ fn generates_list_content_variant(parsed: &ParsedNodeFn) -> bool { fn generate_register_node_impl( parsed: &ParsedNodeFn, + regular_fields: &[&ParsedField], field_names: &[&Ident], struct_name: &Ident, mapped_struct_name: &Ident, @@ -1126,7 +1146,6 @@ fn generate_register_node_impl( 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 primary_regular_index = regular_fields.iter().position(|field| !field.is_environment()); let parameter_types: Vec<_> = regular_fields @@ -1162,23 +1181,19 @@ fn generate_register_node_impl( let max_implementations = parameter_types.iter().map(|x| x.len()).chain([parsed.input.implementations.len().max(1)]).max(); - // Element-wise nodes register a variant per wire shape per implementations row; all other nodes register one let gcore = quote!(gcore); - let variants: Vec = if primary_item_element(parsed).is_some() { - let mut variants = vec![RegisterVariant::Item]; - if generates_mapped_variant(parsed) { - variants.push(RegisterVariant::Mapped); - } - if generates_list_content_variant(parsed) { - variants.push(RegisterVariant::ListContent); - } - variants - } else if is_generator_frame(parsed) { - // A `()` generator registers an Item form (single generation) plus a mapped form framed over its ranked params - vec![RegisterVariant::Item, RegisterVariant::Mapped] + let mut variants = Vec::new(); + if primary_item_element(parsed).is_some() || is_generator_frame(parsed) { + variants.push(RegisterVariant::Item); } else { - vec![RegisterVariant::Plain] + variants.push(RegisterVariant::Plain); }; + if generates_mapped_variant(parsed) { + variants.push(RegisterVariant::Mapped); + } + if generates_list_content_variant(parsed) { + variants.push(RegisterVariant::ListContent); + } for i in 0..max_implementations.unwrap_or(0) { for &variant in &variants { diff --git a/node-graph/node-macro/src/parsing.rs b/node-graph/node-macro/src/parsing.rs index f92d7c73478..d8ff47dae42 100644 --- a/node-graph/node-macro/src/parsing.rs +++ b/node-graph/node-macro/src/parsing.rs @@ -66,6 +66,7 @@ pub enum ParsedValueSource { None, Default(TokenStream2), Scope(Box), + SourceId, } // #[widget(ParsedWidgetOverride::Hidden)] @@ -125,6 +126,40 @@ pub struct ParsedField { } impl ParsedField { + pub(crate) fn hidden(name: &str, ty: Type, value_source: ParsedValueSource) -> Self { + Self { + pat_ident: PatIdent { + attrs: Vec::new(), + by_ref: None, + mutability: None, + ident: Ident::new(name, proc_macro2::Span::call_site()), + subpat: None, + }, + name: None, + description: String::new(), + widget_override: ParsedWidgetOverride::None, + ty: ParsedFieldType::classify(RegularParsedField { + ty, + exposed: false, + value_source, + number_soft_min: None, + number_soft_max: None, + number_hard_min: None, + number_hard_max: None, + number_mode_range: false, + implementations: Default::default(), + gpu_image: false, + narrowed_body_ty: None, + }), + number_display_decimal_places: None, + number_step: None, + unit: None, + multiline: false, + progression: false, + is_data_field: false, + } + } + /// Whether the field is environment rather than an argument: `#[data]` state or a `#[scope]`-injected wire. /// Environment fields never classify the node, never supply the element-wise frame, and broadcast by clone. pub(crate) fn is_environment(&self) -> bool { @@ -132,7 +167,7 @@ impl ParsedField { || matches!( self.ty.regular(), Some(RegularParsedField { - value_source: ParsedValueSource::Scope(_), + value_source: ParsedValueSource::Scope(_) | ParsedValueSource::SourceId, .. }) ) diff --git a/node-graph/preprocessor/src/lib.rs b/node-graph/preprocessor/src/lib.rs index 55f72ef9cf6..1f314b1e761 100644 --- a/node-graph/preprocessor/src/lib.rs +++ b/node-graph/preprocessor/src/lib.rs @@ -138,7 +138,13 @@ impl Preprocessor { for (id, metadata) in core_types::registry::NODE_METADATA.lock().unwrap().iter() { let id = id.clone(); - let NodeMetadata { fields, memoize, inject_scope, .. } = metadata; + let NodeMetadata { + fields, + memoize, + inject_scope, + asynchronous, + .. + } = metadata; context_dependencies.insert(id.clone(), ContextDependencies::from(metadata.context_features.as_slice())); let Some(implementations) = node_registry.get(&id) else { continue }; @@ -253,7 +259,7 @@ impl Preprocessor { }) .collect(); - if generated_nodes == 0 && !memoize && !inject_scope { + if generated_nodes == 0 && !memoize && !inject_scope && !asynchronous { continue; } From 6078782b98de4447def48e18901a93c7a328b7d4 Mon Sep 17 00:00:00 2001 From: Timon Date: Fri, 9 Oct 2026 19:42:37 +0000 Subject: [PATCH 25/54] Classify node return types in the macro --- node-graph/node-macro/src/parsing.rs | 59 ++++++++++++++++++++++++- node-graph/node-macro/src/validation.rs | 7 +-- 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/node-graph/node-macro/src/parsing.rs b/node-graph/node-macro/src/parsing.rs index d8ff47dae42..2d4a82f9658 100644 --- a/node-graph/node-macro/src/parsing.rs +++ b/node-graph/node-macro/src/parsing.rs @@ -650,7 +650,7 @@ pub(crate) fn parse_node_fn(attr: TokenStream2, item: TokenStream2) -> syn::Resu narrow_shader_fields(&mut fields)?; } let output_type = parse_output(&input_fn.sig.output)?; - let output_element = peel_item(&output_type); + let output_element = peel_item(&output_payload(&output_type)); let where_clause = input_fn.sig.generics.where_clause; let body = input_fn.block.to_token_stream(); let description = input_fn @@ -1184,6 +1184,48 @@ impl ParsedNodeFn { } } +#[derive(Clone, Copy, PartialEq, Debug)] +pub(crate) enum NodeOutputTier { + /// `-> T` + Plain, + /// `-> Result` + Interrupt, + /// `-> Result` + Fallback, + /// `-> Poll` + Poll, +} + +pub(crate) fn output_tier(output_type: &Type) -> NodeOutputTier { + let Type::Path(type_path) = output_type else { return NodeOutputTier::Plain }; + let Some(segment) = type_path.path.segments.last() else { return NodeOutputTier::Plain }; + let syn::PathArguments::AngleBracketed(arguments) = &segment.arguments else { + return NodeOutputTier::Plain; + }; + match (segment.ident.to_string().as_str(), arguments.args.len()) { + ("Poll", 1) => NodeOutputTier::Poll, + ("Result", 2) => match arguments.args.last() { + Some(syn::GenericArgument::Type(Type::Tuple(_))) => NodeOutputTier::Fallback, + _ => NodeOutputTier::Interrupt, + }, + _ => NodeOutputTier::Plain, + } +} + +pub(crate) fn output_payload(output_type: &Type) -> Type { + if output_tier(output_type) == NodeOutputTier::Plain { + return output_type.clone(); + } + let Type::Path(type_path) = output_type else { unreachable!() }; + let syn::PathArguments::AngleBracketed(arguments) = &type_path.path.segments.last().unwrap().arguments else { + unreachable!() + }; + match arguments.args.first() { + Some(syn::GenericArgument::Type(payload)) => payload.clone(), + _ => output_type.clone(), + } +} + #[cfg(test)] mod tests { use super::*; @@ -1954,4 +1996,19 @@ mod tests { ); assert!(parse_node_fn(attr, declared_list).is_err(), "a uniform cannot carry a List"); } + + #[test] + fn a_kernel_is_classified_by_its_declared_return_type() { + let payload: Type = parse_quote!(Item); + for (ty, tier) in [ + (parse_quote!(Item), NodeOutputTier::Plain), + (parse_quote!(Result, Interrupt>), NodeOutputTier::Interrupt), + (parse_quote!(Result, (Item, NodeError)>), NodeOutputTier::Fallback), + (parse_quote!(Poll>), NodeOutputTier::Poll), + ] { + let ty: Type = ty; + assert_eq!(output_tier(&ty), tier); + assert_eq!(output_payload(&ty), payload); + } + } } diff --git a/node-graph/node-macro/src/validation.rs b/node-graph/node-macro/src/validation.rs index aa7c55f7503..5d432c57533 100644 --- a/node-graph/node-macro/src/validation.rs +++ b/node-graph/node-macro/src/validation.rs @@ -1,4 +1,4 @@ -use crate::parsing::{Implementation, NodeParsedField, ParsedFieldType, ParsedNodeFn, RegularParsedField}; +use crate::parsing::{Implementation, NodeParsedField, ParsedFieldType, ParsedNodeFn, RegularParsedField, output_payload}; use proc_macro_error2::emit_error; use quote::quote; use syn::spanned::Spanned; @@ -75,6 +75,7 @@ fn validate_element_wise(parsed: &ParsedNodeFn) { if parsed.attributes.skip_impl { return; } + let output = output_payload(&parsed.output_type); let Some((_, primary)) = parsed.primary_input_field() else { return }; let Some(RegularParsedField { ty, implementations, .. }) = primary.ty.regular() else { @@ -88,7 +89,7 @@ fn validate_element_wise(parsed: &ParsedNodeFn) { || primary.ty.list_element().is_some() || outer_wrapper_is(ty, "ListDyn") || implementations.iter().any(|ty| outer_wrapper_is(ty, "List") || outer_wrapper_is(ty, "ListDyn")); - if outer_wrapper_is(&parsed.output_type, "Item") && !primary_reduces_or_generates { + if outer_wrapper_is(&output, "Item") && !primary_reduces_or_generates { emit_error!( parsed.output_type.span(), "Returning `Item` requires the primary input to be `Item` (element-wise), `List`/`ListDyn` (aggregation), or `()` (generator)" @@ -108,7 +109,7 @@ fn validate_element_wise(parsed: &ParsedNodeFn) { ); } - if !outer_wrapper_is(&parsed.output_type, "Item") && !outer_wrapper_is(&parsed.output_type, "List") { + if !outer_wrapper_is(&output, "Item") && !outer_wrapper_is(&output, "List") { emit_error!( parsed.output_type.span(), "An element-wise node (declared by its `Item` primary input) must return `Item`, or `List` for an expander" From 7a9fb61e00a97e41704255f1b138d6d0eb3123dc Mon Sep 17 00:00:00 2001 From: Timon Date: Fri, 9 Oct 2026 19:43:55 +0000 Subject: [PATCH 26/54] Make Node eval sync and return Poll --- editor/src/node_graph_executor/runtime.rs | 26 +- node-graph/graph-craft/src/document/value.rs | 59 ++-- .../graph-craft/src/graphene_compiler.rs | 4 +- .../src/dynamic_executor.rs | 59 ++-- .../interpreted-executor/src/node_registry.rs | 45 ++- .../libraries/core-types/src/generic.rs | 11 +- node-graph/libraries/core-types/src/lib.rs | 74 ++--- node-graph/libraries/core-types/src/ops.rs | 15 +- node-graph/libraries/core-types/src/poll.rs | 29 ++ .../libraries/core-types/src/registry.rs | 112 +++----- node-graph/libraries/core-types/src/value.rs | 21 +- node-graph/node-macro/src/codegen.rs | 261 ++++++++++++------ node-graph/node-macro/src/parsing.rs | 50 +++- node-graph/node-macro/src/validation.rs | 6 + node-graph/nodes/gcore/src/animation.rs | 18 +- .../nodes/gcore/src/context_modification.rs | 8 +- node-graph/nodes/gcore/src/memo.rs | 20 +- node-graph/nodes/gstd/src/any.rs | 14 +- .../nodes/gstd/src/platform_application_io.rs | 49 ++-- 19 files changed, 513 insertions(+), 368 deletions(-) diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index 14107f16304..ae837751c22 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -22,7 +22,7 @@ use graphene_std::runtime::AsyncRuntime; use graphene_std::transform::RenderQuality; use graphene_std::vector::Vector; use graphene_std::vector::style::RenderMode; -use graphene_std::{Artboard, Context, Graphic}; +use graphene_std::{Artboard, Context, Graphic, Poll}; use interpreted_executor::dynamic_executor::{DynamicExecutor, IntrospectError, ResolvedDocumentNodeTypesDelta}; use interpreted_executor::util::wrap_network_in_scope; use spin::Mutex; @@ -218,7 +218,7 @@ impl NodeRuntime { render_config.export_format = ExportFormat::Svg; } - let result = self.execute_network(render_config).await; + let result = self.execute_network(render_config); let mut responses = VecDeque::new(); // TODO: Only process monitor nodes if the graph has changed, not when only the Footprint changes if !render_config.for_eyedropper { @@ -358,17 +358,25 @@ impl NodeRuntime { result } - async fn execute_network(&mut self, render_config: RenderConfig) -> Result { + fn execute_network(&mut self, render_config: RenderConfig) -> Result { use graph_craft::graphene_compiler::Executor; - match self.executor.input_type() { + let state = match self.executor.input_type() { Some(t) if t == concrete!(Context) => (&self.executor) .execute(render_config.into_context_with_generations(self.async_runtime().snapshot())) - .await - .map_err(|e| e.to_string()), - Some(t) if t == concrete!(()) => (&self.executor).execute(()).await.map_err(|e| e.to_string()), - Some(t) => Err(format!("Invalid input type {t:?}")), - _ => Err(format!("No input type:\n{:?}", self.node_graph_errors)), + .map_err(|e| e.to_string())?, + Some(t) if t == concrete!(()) => (&self.executor).execute(()).map_err(|e| e.to_string())?, + Some(t) => return Err(format!("Invalid input type {t:?}")), + _ => return Err(format!("No input type:\n{:?}", self.node_graph_errors)), + }; + match state { + Poll::Final(value) | Poll::Partial(value) => Ok(value), + Poll::Fallback(boxed) => { + let (value, error) = *boxed; + Ok(value) + } + Poll::Pending => Err("Node graph evaluation is pending".to_string()), + Poll::Error(error) => Err(format!("Node graph evaluation failed: {error}")), } } diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 52281b906d9..01c9ba9b1ab 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -1,14 +1,13 @@ use super::DocumentNode; use crate::application_io::PlatformEditorApi; use crate::application_io::resource::Resource; -use crate::proto::{Any as DAny, FutureAny}; +use crate::proto::{Any as DAny, AnyInput}; use brush_nodes::{BrushCache, Stroke}; use core_types::color::SRGBA8; use core_types::list::{Item, List, NodeIdPath}; use core_types::transfer_curve::TransferCurve; use core_types::transform::Footprint; -use core_types::{CacheHash, Color, ContextModification, MemoHash, Node, RuntimeHandle, SourceId, Type, TypeDescriptor}; -use dyn_any::DynAny; +use core_types::{CacheHash, Color, ContextModification, MemoHash, Node, Poll, RuntimeHandle, SourceId, Type, TypeDescriptor}; pub use dyn_any::StaticType; pub use glam::{DAffine2, DVec2, IVec2, UVec2}; use graphene_application_io::resource::ResourceHash; @@ -161,9 +160,9 @@ macro_rules! tagged_value { } } - impl<'a> TaggedValue { - /// Converts to a Box - pub fn to_dynany(self) -> DAny<'a> { + impl TaggedValue { + /// Converts to a `Box` + pub fn to_dynany(self) -> DAny { match self { // =============== // MANUAL VARIANTS @@ -341,39 +340,38 @@ macro_rules! tagged_value { } /// Attempts to downcast the dynamic type to a tagged value - pub fn try_from_any(input: Box + 'a>) -> Result { - use dyn_any::downcast; + pub fn try_from_any(input: DAny) -> Result { use std::any::TypeId; - match DynAny::type_id(input.as_ref()) { + match (*input).type_id() { // =============== // MANUAL VARIANTS // =============== // The manual variants convert from both their payload and wire forms, with the newtypes flattening to their stored `Vec` form x if x == TypeId::of::<()>() => Ok(TaggedValue::None), - x if x == TypeId::of::>() => Ok(TaggedValue::Numbers(*downcast(input).unwrap())), - x if x == TypeId::of::>() => Ok(TaggedValue::Numbers(downcast::>(input).unwrap().iter_element_values().copied().collect())), - x if x == TypeId::of::() => Ok(TaggedValue::DashPattern(downcast::(input).unwrap().0.iter_element_values().copied().collect())), - x if x == TypeId::of::>() => Ok(TaggedValue::DashPattern(downcast::>(input).unwrap().into_element().0.iter_element_values().copied().collect())), - x if x == TypeId::of::() => Ok(TaggedValue::BoxCorners(downcast::(input).unwrap().0.iter_element_values().copied().collect())), - x if x == TypeId::of::>() => Ok(TaggedValue::BoxCorners(downcast::>(input).unwrap().into_element().0.iter_element_values().copied().collect())), - x if x == TypeId::of::() => Ok(TaggedValue::TransferCurve(downcast::(input).unwrap().points().to_vec())), - x if x == TypeId::of::>() => Ok(TaggedValue::TransferCurve(downcast::>(input).unwrap().into_element().points().to_vec())), - x if x == TypeId::of::() => Ok(TaggedValue::GradientRamp(GradientRamp::from(*downcast::(input).unwrap()))), - x if x == TypeId::of::>() => Ok(TaggedValue::GradientRamp(GradientRamp::from(&*downcast::>(input).unwrap()))), - x if x == TypeId::of::>() => Ok(TaggedValue::Strokes(downcast::>(input).unwrap().into_iter().map(Item::into_element).collect())), - x if x == TypeId::of::>() => Ok(TaggedValue::BrushCache(downcast::>(input).unwrap().into_element())), + x if x == TypeId::of::>() => Ok(TaggedValue::Numbers(*input.downcast().unwrap())), + x if x == TypeId::of::>() => Ok(TaggedValue::Numbers(input.downcast::>().unwrap().iter_element_values().copied().collect())), + x if x == TypeId::of::() => Ok(TaggedValue::DashPattern(input.downcast::().unwrap().0.iter_element_values().copied().collect())), + x if x == TypeId::of::>() => Ok(TaggedValue::DashPattern(input.downcast::>().unwrap().into_element().0.iter_element_values().copied().collect())), + x if x == TypeId::of::() => Ok(TaggedValue::BoxCorners(input.downcast::().unwrap().0.iter_element_values().copied().collect())), + x if x == TypeId::of::>() => Ok(TaggedValue::BoxCorners(input.downcast::>().unwrap().into_element().0.iter_element_values().copied().collect())), + x if x == TypeId::of::() => Ok(TaggedValue::TransferCurve(input.downcast::().unwrap().points().to_vec())), + x if x == TypeId::of::>() => Ok(TaggedValue::TransferCurve(input.downcast::>().unwrap().into_element().points().to_vec())), + x if x == TypeId::of::() => Ok(TaggedValue::GradientRamp(GradientRamp::from(*input.downcast::().unwrap()))), + x if x == TypeId::of::>() => Ok(TaggedValue::GradientRamp(GradientRamp::from(&*input.downcast::>().unwrap()))), + x if x == TypeId::of::>() => Ok(TaggedValue::Strokes(input.downcast::>().unwrap().into_iter().map(Item::into_element).collect())), + x if x == TypeId::of::>() => Ok(TaggedValue::BrushCache(input.downcast::>().unwrap().into_element())), // ======================= // AUTO-GENERATED VARIANTS // ======================= - $( x if x == TypeId::of::<$ty>() => Ok(TaggedValue::$identifier(*downcast(input).unwrap())), )* - $( x if x == TypeId::of::>() => Ok(TaggedValue::$identifier(downcast::>(input).unwrap().into_element())), )* + $( x if x == TypeId::of::<$ty>() => Ok(TaggedValue::$identifier(*input.downcast().unwrap())), )* + $( x if x == TypeId::of::>() => Ok(TaggedValue::$identifier(input.downcast::>().unwrap().into_element())), )* // ======================= // NON-SERIALIZED VARIANTS // ======================= - x if x == TypeId::of::>() => Ok(TaggedValue::RenderOutput(downcast::>(input).unwrap().into_element())), + x if x == TypeId::of::>() => Ok(TaggedValue::RenderOutput(input.downcast::>().unwrap().into_element())), - _ => Err(format!("Cannot convert {:?} to TaggedValue", DynAny::type_name(input.as_ref()))), + _ => Err(format!("Cannot convert {:?} to TaggedValue", (*input).type_id())), } } @@ -878,12 +876,11 @@ impl Display for TaggedValue { pub struct UpcastNode { value: MemoHash, } -impl<'input> Node<'input, DAny<'input>> for UpcastNode { - type Output = FutureAny<'input>; +impl Node for UpcastNode { + type Output = DAny; - fn eval(&'input self, _: DAny<'input>) -> Self::Output { - let memo_clone = MemoHash::clone(&self.value); - Box::pin(async move { memo_clone.into_inner().as_ref().clone().to_dynany() }) + fn eval(&self, _: &AnyInput) -> Poll { + Poll::Final((*self.value).clone().to_dynany()) } } impl UpcastNode { @@ -965,7 +962,7 @@ mod typedefault_dispatch { let expected_type_id = std::any::TypeId::of::<$type_default>(); let dyn_value = TaggedValue::TypeDefault(ty.clone()).to_dynany(); assert_eq!( - DynAny::type_id(&*dyn_value), + (*dyn_value).type_id(), expected_type_id, "`to_dynany(TypeDefault({0}))` did not produce a `{0}` — the type-default lists cover this type but the unwrap site doesn't handle it. Without a match, `to_dynany` falls back to `from_type_or_none`, which returns `TypeDefault({0})` again and recurses forever.", std::any::type_name::<$type_default>(), diff --git a/node-graph/graph-craft/src/graphene_compiler.rs b/node-graph/graph-craft/src/graphene_compiler.rs index 22dac658a95..fbf5be5a658 100644 --- a/node-graph/graph-craft/src/graphene_compiler.rs +++ b/node-graph/graph-craft/src/graphene_compiler.rs @@ -1,5 +1,5 @@ use crate::document::NodeNetwork; -use crate::proto::{LocalFuture, ProtoNetwork}; +use crate::proto::ProtoNetwork; use std::error::Error; pub struct Compiler {} @@ -33,5 +33,5 @@ impl Compiler { } pub trait Executor { - fn execute(&self, input: I) -> LocalFuture<'_, Result>>; + fn execute(&self, input: I) -> Result>; } diff --git a/node-graph/interpreted-executor/src/dynamic_executor.rs b/node-graph/interpreted-executor/src/dynamic_executor.rs index 2cb28b36bda..32d4e80f235 100644 --- a/node-graph/interpreted-executor/src/dynamic_executor.rs +++ b/node-graph/interpreted-executor/src/dynamic_executor.rs @@ -1,11 +1,10 @@ use crate::node_registry; -use core_types::SourceId; -use dyn_any::StaticType; +use core_types::{Poll, SourceId, WasmNotSend}; use graph_craft::Type; use graph_craft::document::NodeId; use graph_craft::document::value::{TaggedValue, UpcastNode}; use graph_craft::graphene_compiler::Executor; -use graph_craft::proto::{ConstructionArgs, GraphError, LocalFuture, NodeContainer, ProtoNetwork, ProtoNode, SharedNodeContainer, TypeErasedBox, TypingContext}; +use graph_craft::proto::{ConstructionArgs, GraphError, NodeContainer, ProtoNetwork, ProtoNode, SharedNodeContainer, TypeErasedBox, TypingContext}; use graph_craft::proto::{GraphErrorType, GraphErrors}; use std::collections::{HashMap, HashSet}; use std::error::Error; @@ -141,25 +140,18 @@ impl DynamicExecutor { } } -impl Executor for &DynamicExecutor +impl Executor> for &DynamicExecutor where - I: StaticType + 'static + Send + Sync, + I: 'static + WasmNotSend, { - fn execute(&self, input: I) -> LocalFuture<'_, Result>> { - Box::pin(async move { - use futures::FutureExt; - - let result = self.tree.eval_tagged_value(self.output, input); - let wrapped_result = std::panic::AssertUnwindSafe(result).catch_unwind().await; - - match wrapped_result { - Ok(result) => result.map_err(|e| e.into()), - Err(e) => { - Box::leak(e); - Err("Node graph execution panicked".into()) - } + fn execute(&self, input: I) -> Result, Box> { + match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| self.tree.eval_tagged_value(self.output, &input))) { + Ok(result) => result.map_err(|e| e.into()), + Err(e) => { + Box::leak(e); + Err("Node graph execution panicked".into()) } - }) + } } } pub struct InputMapping {} @@ -255,24 +247,31 @@ impl BorrowTree { } /// Evaluate the output node of the [`BorrowTree`]. - pub async fn eval<'i, I, O>(&'i self, id: NodeId, input: I) -> Option + pub fn eval(&self, id: NodeId, input: &I) -> Option where - I: StaticType + 'i + Send + Sync, - O: StaticType + 'i, + I: 'static + WasmNotSend, + O: 'static, { - let (node, _path) = self.nodes.get(&id).cloned()?; - let output = node.eval(Box::new(input)); - dyn_any::downcast::(output.await).ok().map(|o| *o) + let (node, _path) = self.nodes.get(&id)?; + node.eval(input).value().and_then(|output| output.downcast::().ok().map(|o| *o)) } /// Evaluate the output node of the [`BorrowTree`] and cast it to a tagged value. /// This ensures that no borrowed data can escape the node graph. - pub async fn eval_tagged_value(&self, id: NodeId, input: I) -> Result + pub fn eval_tagged_value(&self, id: NodeId, input: &I) -> Result, String> where - I: StaticType + 'static + Send + Sync, + I: 'static + WasmNotSend, { - let (node, _path) = self.nodes.get(&id).cloned().ok_or("Output node not found in executor")?; - let output = node.eval(Box::new(input)); - TaggedValue::try_from_any(output.await) + let (node, _path) = self.nodes.get(&id).ok_or("Output node not found in executor")?; + Ok(match node.eval(input) { + Poll::Pending => Poll::Pending, + Poll::Final(output) => Poll::Final(TaggedValue::try_from_any(output)?), + Poll::Partial(output) => Poll::Partial(TaggedValue::try_from_any(output)?), + Poll::Fallback(boxed) => { + let (output, error) = *boxed; + Poll::Fallback(Box::new((TaggedValue::try_from_any(output)?, error))) + } + Poll::Error(error) => Poll::Error(error), + }) } /// Removes a node from the [`BorrowTree`] and returns its associated path. diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index 7708b632537..2731e89468e 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -168,9 +168,9 @@ fn node_registry() -> HashMap + Send>>>::new()); + let node = graphene_core::ops::PassthroughNode::new(graphene_std::any::PanicNode::::new()); let params = vec![fn_type_fut!(Context, $ty)]; - let node_io = NodeIO::<'_, Context>::to_async_node_io(&node, params); + let node_io = NodeIO::::to_async_node_io(&node, params); node_io }, ) @@ -181,18 +181,18 @@ fn node_registry() -> HashMap(args.pop().unwrap()), - graphene_std::any::FutureWrapperNode::new(graphene_std::value::ClonedNode::new(std::marker::PhantomData::<$to>)), + graphene_std::value::ClonedNode::new(std::marker::PhantomData::<$to>), ); let any: DynAnyNode = graphene_std::any::DynAnyNode::new(node); Box::new(any) as TypeErasedBox }, { let node = graphene_core::ops::IntoNode::new( - graphene_std::any::PanicNode:: + Send>>>::new(), - graphene_std::any::FutureWrapperNode::new(graphene_std::value::ClonedNode::new(std::marker::PhantomData::<$to>)), + graphene_std::any::PanicNode::::new(), + graphene_std::value::ClonedNode::new(std::marker::PhantomData::<$to>), ); let params = vec![fn_type_fut!(Context, $from)]; - let node_io = NodeIO::<'_, Context>::to_async_node_io(&node, params); + let node_io = NodeIO::::to_async_node_io(&node, params); node_io }, ) @@ -214,18 +214,18 @@ fn node_registry() -> HashMap(args.pop().unwrap()), - graphene_std::any::FutureWrapperNode::new(graphene_std::value::ClonedNode::new(std::marker::PhantomData::<$element>)), + graphene_std::value::ClonedNode::new(std::marker::PhantomData::<$element>), ); let any: DynAnyNode = graphene_std::any::DynAnyNode::new(node); Box::new(any) as TypeErasedBox }, { let node = graphene_core::ops::$node::new( - graphene_std::any::PanicNode:: + Send>>>::new(), - graphene_std::any::FutureWrapperNode::new(graphene_std::value::ClonedNode::new(std::marker::PhantomData::<$element>)), + graphene_std::any::PanicNode::::new(), + graphene_std::value::ClonedNode::new(std::marker::PhantomData::<$element>), ); let params = vec![fn_type_fut!(Context, $from)]; - let node_io = NodeIO::<'_, Context>::to_async_node_io(&node, params); + let node_io = NodeIO::::to_async_node_io(&node, params); node_io }, ) @@ -242,12 +242,9 @@ fn node_registry() -> HashMap> + Send>>, - >::new()); + let node = graphene_core::ops::ItemToListNode::new(graphene_std::any::PanicNode::>::new()); let params = vec![fn_type_fut!(Context, Item<$element>)]; - let node_io = NodeIO::<'_, Context>::to_async_node_io(&node, params); + let node_io = NodeIO::::to_async_node_io(&node, params); node_io }, ) @@ -264,12 +261,9 @@ fn node_registry() -> HashMap> + Send>>, - >::new()); + let node = graphene_core::ops::BundleNode::new(graphene_std::any::PanicNode::>::new()); let params = vec![fn_type_fut!(Context, List<$element>)]; - let node_io = NodeIO::<'_, Context>::to_async_node_io(&node, params); + let node_io = NodeIO::::to_async_node_io(&node, params); node_io }, ) @@ -286,12 +280,9 @@ fn node_registry() -> HashMap>> + Send>>, - >::new()); + let node = graphene_core::ops::UnbundleNode::new(graphene_std::any::PanicNode::>>::new()); let params = vec![fn_type_fut!(Context, Item>)]; - let node_io = NodeIO::<'_, Context>::to_async_node_io(&node, params); + let node_io = NodeIO::::to_async_node_io(&node, params); node_io }, ) @@ -645,10 +636,10 @@ mod node_registry_macros { }, { let node = <$path>::new($( - graphene_std::any::PanicNode::<$arg, core::pin::Pin + Send>>>::new() + graphene_std::any::PanicNode::<$arg, $type>::new() ),*); let params = vec![$(fn_type_fut!($arg, $type)),*]; - let mut node_io = NodeIO::<'_, $input>::to_async_node_io(&node, params); + let mut node_io = NodeIO::<$input>::to_async_node_io(&node, params); node_io.call_argument = concrete!(<$input as StaticType>::Static); node_io }, diff --git a/node-graph/libraries/core-types/src/generic.rs b/node-graph/libraries/core-types/src/generic.rs index 055c0cb53dc..8ebfc78ff46 100644 --- a/node-graph/libraries/core-types/src/generic.rs +++ b/node-graph/libraries/core-types/src/generic.rs @@ -1,16 +1,17 @@ use crate::Node; +use crate::poll::Poll; use std::marker::PhantomData; #[derive(Clone)] -pub struct FnNode O, I, O>(T, PhantomData<(I, O)>); +pub struct FnNode O, I, O>(T, PhantomData<(I, O)>); -impl<'i, T: Fn(I) -> O + 'i, O: 'i, I: 'i> Node<'i, I> for FnNode { +impl O, I, O> Node for FnNode { type Output = O; - fn eval(&'i self, input: I) -> Self::Output { - self.0(input) + fn eval(&self, input: &I) -> Poll { + Poll::Final(self.0(input)) } } -impl O, I, O> FnNode { +impl O, I, O> FnNode { pub fn new(f: T) -> Self { FnNode(f, PhantomData) } diff --git a/node-graph/libraries/core-types/src/lib.rs b/node-graph/libraries/core-types/src/lib.rs index 3b723fd3e10..f7bec4eec96 100644 --- a/node-graph/libraries/core-types/src/lib.rs +++ b/node-graph/libraries/core-types/src/lib.rs @@ -41,21 +41,19 @@ pub use no_std_types::choice_type; pub use no_std_types::color; pub use no_std_types::shaders; pub use num_traits; +pub use poll::{Finality, Interrupt, LazyInput, NodeError, Poll, Status}; pub use runtime::{Generations, RuntimeHandle, SourceId}; use std::any::TypeId; -use std::future::Future; -use std::pin::Pin; #[cfg(feature = "wasm")] pub use tsify; pub use types::Cow; -// pub trait Node: for<'n> NodeIO<'n> { /// The node trait allows for defining any node. Nodes can only take one call argument input, however they can store references to other nodes inside the struct. /// See `node-graph/README.md` for information on how to define a new node. -pub trait Node<'i, Input> { - type Output: 'i; +pub trait Node { + type Output; /// Evaluates the node with the single specified input. - fn eval(&'i self, input: Input) -> Self::Output; + fn eval(&self, input: &Input) -> Poll; /// Returns the name of the node for diagnostic purposes. fn node_name(&self) -> &'static str { std::any::type_name::() @@ -70,9 +68,9 @@ pub trait Node<'i, Input> { mod types; pub use types::*; -pub trait NodeIO<'i, Input>: Node<'i, Input> +pub trait NodeIO: Node where - Self::Output: 'i + StaticTypeSized, + Self::Output: StaticTypeSized, Input: StaticTypeSized, { fn input_type(&self) -> TypeId { @@ -94,56 +92,60 @@ where inputs, } } - fn to_async_node_io(&self, inputs: Vec) -> NodeIOTypes - where - ::Output: StaticTypeSized, - Self::Output: Future, - { + fn to_async_node_io(&self, inputs: Vec) -> NodeIOTypes { NodeIOTypes { call_argument: concrete!(::Static), - return_value: future!(<::Output as StaticTypeSized>::Static), + return_value: future!(::Static), inputs, } } } -impl<'i, N: Node<'i, I>, I> NodeIO<'i, I> for N +impl, I> NodeIO for N where - N::Output: 'i + StaticTypeSized, + N::Output: StaticTypeSized, I: StaticTypeSized, { } -impl<'i, I: 'i, N: Node<'i, I> + ?Sized> Node<'i, I> for &'i N { +impl + ?Sized> Node for &N { type Output = N::Output; - fn eval(&'i self, input: I) -> N::Output { - (*self).eval(input) - } -} -impl<'i, I: 'i, O: 'i, N: Node<'i, I, Output = O> + ?Sized> Node<'i, I> for Box { - type Output = O; - fn eval(&'i self, input: I) -> O { + #[inline(always)] + fn eval(&self, input: &I) -> Poll { (**self).eval(input) } -} -impl<'i, I: 'i, O: 'i, N: Node<'i, I, Output = O> + ?Sized> Node<'i, I> for std::sync::Arc { - type Output = O; - fn eval(&'i self, input: I) -> O { - (**self).eval(input) + fn node_name(&self) -> &'static str { + (**self).node_name() + } + fn serialize(&self) -> Option> { + (**self).serialize() } } - -impl<'i, I, O: 'i> Node<'i, I> for Pin + 'i>> { - type Output = O; - fn eval(&'i self, input: I) -> O { +impl + ?Sized> Node for Box { + type Output = N::Output; + #[inline(always)] + fn eval(&self, input: &I) -> Poll { (**self).eval(input) } + fn node_name(&self) -> &'static str { + (**self).node_name() + } + fn serialize(&self) -> Option> { + (**self).serialize() + } } -impl<'i, I, O: 'i> Node<'i, I> for Pin<&'i (dyn NodeIO<'i, I, Output = O> + 'i)> { - type Output = O; - fn eval(&'i self, input: I) -> O { +impl + ?Sized> Node for std::sync::Arc { + type Output = N::Output; + #[inline(always)] + fn eval(&self, input: &I) -> Poll { (**self).eval(input) } + fn node_name(&self) -> &'static str { + (**self).node_name() + } + fn serialize(&self) -> Option> { + (**self).serialize() + } } /// A compile-time symbol naming one parameter of one proto node. diff --git a/node-graph/libraries/core-types/src/ops.rs b/node-graph/libraries/core-types/src/ops.rs index 5d3cf37fd97..f5c06e7febc 100644 --- a/node-graph/libraries/core-types/src/ops.rs +++ b/node-graph/libraries/core-types/src/ops.rs @@ -1,5 +1,6 @@ use crate::Node; use crate::math::float_noise::round_away_float_noise; +use crate::poll::Poll; use crate::transform::Footprint; use glam::{DAffine2, DVec2}; use std::marker::PhantomData; @@ -7,13 +8,13 @@ use std::marker::PhantomData; // Type // TODO: Document this #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] -pub struct TypeNode Node<'a, I>, I, O>(pub N, pub PhantomData<(I, O)>); -impl<'i, N, I: 'i, O: 'i> Node<'i, I> for TypeNode +pub struct TypeNode, I, O>(pub N, pub PhantomData<(I, O)>); +impl Node for TypeNode where - N: for<'n> Node<'n, I, Output = O>, + N: Node, { type Output = O; - fn eval(&'i self, input: I) -> Self::Output { + fn eval(&self, input: &I) -> Poll { self.0.eval(input) } @@ -21,17 +22,17 @@ where self.0.serialize() } } -impl<'i, N: for<'a> Node<'a, I>, I: 'i> TypeNode>::Output> { +impl, I> TypeNode>::Output> { pub fn new(node: N) -> Self { Self(node, PhantomData) } } -impl<'i, N: for<'a> Node<'a, I> + Clone, I: 'i> Clone for TypeNode>::Output> { +impl + Clone, I> Clone for TypeNode>::Output> { fn clone(&self) -> Self { Self(self.0.clone(), self.1) } } -impl<'i, N: for<'a> Node<'a, I> + Copy, I: 'i> Copy for TypeNode>::Output> {} +impl + Copy, I> Copy for TypeNode>::Output> {} /// The [`Convert`] trait allows for conversion between Rust primitive numeric types. /// Because number casting is lossy, we cannot use the normal [`Into`] trait like we do for other types. diff --git a/node-graph/libraries/core-types/src/poll.rs b/node-graph/libraries/core-types/src/poll.rs index 82edf0dd0f1..55445831038 100644 --- a/node-graph/libraries/core-types/src/poll.rs +++ b/node-graph/libraries/core-types/src/poll.rs @@ -1,3 +1,4 @@ +use crate::Node; use std::cell::Cell; use std::error::Error; use std::sync::Arc; @@ -214,6 +215,34 @@ impl Status { } } +pub struct LazyInput<'a, N> { + node: &'a N, + status: &'a Status, + index: usize, +} + +impl Clone for LazyInput<'_, N> { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for LazyInput<'_, N> {} + +impl<'a, N> LazyInput<'a, N> { + pub fn new(node: &'a N, status: &'a Status, index: usize) -> Self { + Self { node, status, index } + } + + #[inline(always)] + pub fn eval(&self, input: Input) -> Result + where + N: Node, + { + self.status.record(self.index, self.node.eval(&input)) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/node-graph/libraries/core-types/src/registry.rs b/node-graph/libraries/core-types/src/registry.rs index 3f81c5016ae..1cecc6c1c2b 100644 --- a/node-graph/libraries/core-types/src/registry.rs +++ b/node-graph/libraries/core-types/src/registry.rs @@ -1,9 +1,8 @@ -use crate::{Color, ContextFeature, Node, NodeIO, NodeIOTypes, ProtoNodeIdentifier, Type, WasmNotSend}; -use dyn_any::{DynAny, StaticType}; +use crate::poll::Poll; +use crate::{Color, ContextFeature, Node, NodeIOTypes, ProtoNodeIdentifier, Type, WasmNotSend}; use std::collections::HashMap; use std::marker::PhantomData; use std::ops::Deref; -use std::pin::Pin; use std::sync::{LazyLock, Mutex}; // Translation struct between macro and definition @@ -71,24 +70,20 @@ pub static NODE_REGISTRY: NodeRegistry = LazyLock::new(|| Mutex::new(HashMap::ne pub static NODE_METADATA: LazyLock>> = LazyLock::new(|| Mutex::new(HashMap::new())); #[cfg(not(target_family = "wasm"))] -pub type DynFuture<'n, T> = Pin + 'n + Send>>; +pub type Any = Box; #[cfg(target_family = "wasm")] -pub type DynFuture<'n, T> = Pin + 'n>>; -pub type LocalFuture<'n, T> = Pin + 'n>>; +pub type Any = Box; #[cfg(not(target_family = "wasm"))] -pub type Any<'n> = Box + 'n + Send>; +pub type AnyInput = dyn std::any::Any + Send; #[cfg(target_family = "wasm")] -pub type Any<'n> = Box + 'n>; -pub type FutureAny<'n> = DynFuture<'n, Any<'n>>; +pub type AnyInput = dyn std::any::Any; // TODO: is this safe? This is assumed to be send+sync. #[cfg(not(target_family = "wasm"))] -pub type TypeErasedNode<'n> = dyn for<'i> NodeIO<'i, Any<'i>, Output = FutureAny<'i>> + 'n + Send + Sync; +pub type TypeErasedNode<'n> = dyn Node + 'n + Send + Sync; #[cfg(target_family = "wasm")] -pub type TypeErasedNode<'n> = dyn for<'i> NodeIO<'i, Any<'i>, Output = FutureAny<'i>> + 'n; -pub type TypeErasedPinnedRef<'n> = Pin<&'n TypeErasedNode<'n>>; +pub type TypeErasedNode<'n> = dyn Node + 'n; pub type TypeErasedRef<'n> = &'n TypeErasedNode<'n>; pub type TypeErasedBox<'n> = Box>; -pub type TypeErasedPinned<'n> = Pin>>; pub type SharedNodeContainer = std::sync::Arc; @@ -152,32 +147,27 @@ impl NodeContainer { } } -/// Boxes the input and downcasts the output. -/// Wraps around a node taking Box and returning Box +/// Erases the input and downcasts the output. +/// Wraps around a node taking `&dyn Any` and returning `Box` #[derive(Clone)] pub struct DowncastBothNode { node: SharedNodeContainer, _i: PhantomData, _o: PhantomData, } -impl<'input, O, I> Node<'input, I> for DowncastBothNode +impl Node for DowncastBothNode where - O: 'input + StaticType + WasmNotSend, - I: 'input + StaticType + WasmNotSend, + I: 'static + WasmNotSend, + O: 'static + WasmNotSend, { - type Output = DynFuture<'input, O>; + type Output = O; #[inline] - #[track_caller] - fn eval(&'input self, input: I) -> Self::Output { - { - let node_name = self.node.node_name(); - let input = Box::new(input); - let future = self.node.eval(input); - Box::pin(async move { - let out = dyn_any::downcast(future.await).unwrap_or_else(|e| panic!("DowncastBothNode wrong output type: {e} in: \n{node_name}")); - *out - }) - } + fn eval(&self, input: &I) -> Poll { + self.node.eval(input).map(|output| { + *output + .downcast::() + .unwrap_or_else(|_| panic!("DowncastBothNode wrong output type: expected {} in: \n{}", std::any::type_name::(), self.node.node_name())) + }) } fn serialize(&self) -> Option> { @@ -193,32 +183,6 @@ impl DowncastBothNode { } } } -pub struct FutureWrapperNode { - node: Node, -} - -impl<'i, T: 'i + WasmNotSend, N> Node<'i, T> for FutureWrapperNode -where - N: Node<'i, T, Output: WasmNotSend> + WasmNotSend, -{ - type Output = DynFuture<'i, N::Output>; - #[inline(always)] - fn eval(&'i self, input: T) -> Self::Output { - let result = self.node.eval(input); - Box::pin(async move { result }) - } - - #[inline(always)] - fn serialize(&self) -> Option> { - self.node.serialize() - } -} - -impl FutureWrapperNode { - pub const fn new(node: N) -> Self { - Self { node } - } -} pub struct DynAnyNode { node: Node, @@ -226,36 +190,26 @@ pub struct DynAnyNode { _o: PhantomData, } -impl<'input, I, O, N> Node<'input, Any<'input>> for DynAnyNode +impl Node for DynAnyNode where - I: 'input + StaticType + WasmNotSend, - O: 'input + StaticType + WasmNotSend, - N: 'input + Node<'input, I, Output = DynFuture<'input, O>>, + I: 'static, + O: 'static + WasmNotSend, + N: Node, { - type Output = FutureAny<'input>; + type Output = Any; #[inline] - fn eval(&'input self, input: Any<'input>) -> Self::Output { - let node_name = std::any::type_name::(); - let output = |input| { - let result = self.node.eval(input); - async move { Box::new(result.await) as Any<'input> } - }; - match dyn_any::downcast(input) { - Ok(input) => Box::pin(output(*input)), - Err(e) => panic!("DynAnyNode Input, {e} in:\n{node_name}"), - } + fn eval(&self, input: &AnyInput) -> Poll { + let input = input + .downcast_ref::() + .unwrap_or_else(|| panic!("DynAnyNode Input: expected {} in:\n{}", std::any::type_name::(), std::any::type_name::())); + self.node.eval(input).map(|output| Box::new(output) as Any) } fn serialize(&self) -> Option> { self.node.serialize() } } -impl<'input, I, O, N> DynAnyNode -where - I: 'input + StaticType, - O: 'input + StaticType, - N: 'input + Node<'input, I, Output = DynFuture<'input, O>>, -{ +impl DynAnyNode { pub const fn new(node: N) -> Self { Self { node, @@ -266,9 +220,9 @@ where } pub struct PanicNode(PhantomData, PhantomData); -impl<'i, I: 'i + WasmNotSend, O: 'i + WasmNotSend> Node<'i, I> for PanicNode { +impl Node for PanicNode { type Output = O; - fn eval(&'i self, _: I) -> Self::Output { + fn eval(&self, _: &I) -> Poll { unimplemented!("This node should never be evaluated") } } diff --git a/node-graph/libraries/core-types/src/value.rs b/node-graph/libraries/core-types/src/value.rs index 4f847012be5..26bdd4e62c6 100644 --- a/node-graph/libraries/core-types/src/value.rs +++ b/node-graph/libraries/core-types/src/value.rs @@ -1,15 +1,15 @@ +use crate::Node; use crate::list::Item; -use crate::registry::DynFuture; -use crate::{Node, WasmNotSend}; +use crate::poll::Poll; #[derive(Clone, Copy)] pub struct ClonedNode(pub T); -impl<'i, T: Clone + 'i, I> Node<'i, I> for ClonedNode { +impl Node for ClonedNode { type Output = T; #[inline(always)] - fn eval(&'i self, _input: I) -> Self::Output { - self.0.clone() + fn eval(&self, _input: &I) -> Poll { + Poll::Final(self.0.clone()) } } @@ -25,17 +25,16 @@ impl From for ClonedNode { } } -/// Yields a precomputed `Item` as a ready future, ignoring its input context. +/// Yields a precomputed `Item`, ignoring its input context. /// Generated list-content variants feed each already-evaluated content slot through this so the kernel's own /// context modifications become no-ops (the slot was evaluated once, up front, at the ambient footprint). pub struct PrecomputedItemNode(pub Item); -impl<'i, T: Clone + WasmNotSend + 'i, I: 'i> Node<'i, I> for PrecomputedItemNode { - type Output = DynFuture<'i, Item>; +impl Node for PrecomputedItemNode { + type Output = Item; #[inline(always)] - fn eval(&'i self, _input: I) -> Self::Output { - let item = self.0.clone(); - Box::pin(async move { item }) + fn eval(&self, _input: &I) -> Poll> { + Poll::Final(self.0.clone()) } } diff --git a/node-graph/node-macro/src/codegen.rs b/node-graph/node-macro/src/codegen.rs index abf80d6ce46..6e61d88e188 100644 --- a/node-graph/node-macro/src/codegen.rs +++ b/node-graph/node-macro/src/codegen.rs @@ -72,6 +72,7 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn data_fields .iter() .any(|field| field.ty.regular().is_some_and(|regular| type_contains_ident(®ular.ty, generic_ident))) + || (parsed.is_async && type_contains_ident(&output_payload(output_type), generic_ident)) }) .cloned() .collect(); @@ -139,9 +140,13 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn quote! { pub(super) #name: #r#gen } }); - let struct_fields: Vec<_> = data_field_defs.chain(regular_field_defs).collect(); - - let mut future_idents = Vec::new(); + let tier = output_tier(output_type); + let output = output_payload(output_type); + if tier == NodeOutputTier::Poll && (mapped_variant || list_content_variant) { + return Err(Error::new_spanned(fn_name, "An element-wise node cannot return `Poll`")); + } + let slot_field = parsed.is_async.then(|| quote! { pub(super) slot: #core_types::runtime::MemoTable<#output> }); + let struct_fields: Vec<_> = data_field_defs.chain(regular_field_defs).chain(slot_field).collect(); // Data fields get passed as references to the underlying function let data_field_idents: Vec<_> = data_fields.iter().map(|f| &f.pat_ident).collect(); @@ -160,9 +165,9 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let field_types: Vec<_> = regular_fields .iter() .map(|field| match &field.ty { - 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>), + ParsedFieldType::Node(NodeParsedField { output_type, input_type, .. }) => match tier { + NodeOutputTier::Poll => parse_quote!(&impl #core_types::Node<#input_type, Output = #output_type>), + _ => parse_quote!(#core_types::LazyInput<'_, impl #core_types::Node<#input_type, Output = #output_type>>), }, value => value.regular().expect("a non-node field is a value field").ty.clone(), }) @@ -326,17 +331,20 @@ 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 eval_args = regular_fields.iter().enumerate().map(|(index, field)| { let name = &field.pat_ident.ident; match &field.ty { - ParsedFieldType::Node { .. } => { - quote! { let #name = &self.#name; } - } - _ => { - quote! { let #name = self.#name.eval(__input.clone()).await; } - } + ParsedFieldType::Node { .. } => match tier { + NodeOutputTier::Poll => quote! { let #name = &self.#name; }, + _ => quote! { let #name = #core_types::LazyInput::new(&self.#name, __cell, #index); }, + }, + _ => quote! { let #name = __cell.record(#index, self.#name.eval(__input)); }, } }); + let unwrap_args = regular_fields.iter().filter(|field| !matches!(field.ty, ParsedFieldType::Node { .. })).map(|field| { + let name = &field.pat_ident.ident; + quote! { let #name = #name?; } + }); // Only regular fields can have min/max constraints let min_max_args = regular_fields.iter().map(|field| match field.ty.regular() { @@ -390,8 +398,6 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn }) .collect() }; - future_idents.extend((0..regular_fields.len()).map(|id| format_ident!("F{}", id))); - // Builds every field's where-clause bounds, wrapping each ranked connector's wire type for the element-wise variants // while environment fields keep their declared type. `list_content` additionally lifts a lazy primary connector's // `Item` output to `List` for the list-content variant. @@ -399,28 +405,21 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn regular_fields .iter() .zip(node_generics.iter()) - .zip(future_idents.iter()) .enumerate() - .map(|(index, ((field, name), fut_ident))| match &field.ty { + .map(|(index, (field, name))| match &field.ty { ParsedFieldType::Node(NodeParsedField { input_type, output_type, output_element, .. }) => { - if !*is_async { - unreachable!("Found node which takes an impl Node<> input but is not async") - } let output_type = if list_content && Some(index) == primary_regular_index { let element_ty = output_element.clone().unwrap_or_else(|| output_type.clone()); WireWrapper::List.apply(core_types, &element_ty) } else { output_type.clone() }; - quote!( - #fut_ident: core::future::Future + #core_types::WasmNotSend + 'n, - #name: #core_types::Node<'n, #input_type, Output = #fut_ident > + #core_types::WasmNotSync - ) + quote!(#name: #core_types::Node<#input_type, Output = #output_type>) } value => { let declared = &value.regular().expect("a non-node field is a value field").ty; @@ -429,12 +428,7 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn (ParsedFieldType::Item { element, .. }, Some(wrap)) if !field.is_environment() => wrap.apply(core_types, element), _ => declared.clone(), }; - let all_lifetime_ty = substitute_lifetimes(ty.clone(), "all"); - 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 - ) + quote!(#name: #core_types::Node<#input_type, Output = #ty>) } }) .collect() @@ -450,13 +444,28 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn #(#field_clauses,)* #(#clampable_clauses,)* #(#extra_clauses,)* - #output_type: 'n, ); struct_where_clause.predicates.extend(extra_where); struct_where_clause }; + + let source_clauses: Vec = if parsed.is_async { + let data_clones = data_fields.iter().filter_map(|field| field.ty.regular()).map(|regular| { + let ty = ®ular.ty; + quote!(#ty: Clone) + }); + [ + quote!(#output: Clone + #core_types::WasmNotSend + #core_types::WasmNotSync + 'static), + quote!(#input_type: #core_types::ExtractAll + #core_types::CloneVarArgs + 'static), + ] + .into_iter() + .chain(data_clones) + .collect() + } else { + Vec::new() + }; let primary_wire = element_wise.then_some(WireWrapper::Item); - let struct_where_clause = make_struct_where_clause(build_field_clauses(primary_wire, false), build_clampable_clauses(primary_wire), Vec::new()); + let struct_where_clause = make_struct_where_clause(build_field_clauses(primary_wire, false), build_clampable_clauses(primary_wire), source_clauses); // The mapped variant clones bare parameters and clones ranked connectors' items per frame slot, so both need Clone let param_clone_clauses: Vec = regular_fields @@ -503,13 +512,13 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let regular_inits = regular_field_names.iter().map(|name| { quote! { #name } }); - let all_field_inits: Vec<_> = data_inits.chain(regular_inits).collect(); + let slot_init = parsed.is_async.then(|| quote! { slot: Default::default() }); + let all_field_inits: Vec<_> = data_inits.chain(regular_inits).chain(slot_init).collect(); 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() { + let struct_derives = if data_fields.is_empty() && !parsed.is_async { quote!(#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]) } else { quote!(#[derive(Debug, Clone)]) @@ -531,20 +540,113 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn use #core_types::misc::Clampable; #(#eval_args)* + #(#unwrap_args)* #(#min_max_args)* }; + let cell_ctor = match attributes.no_partial { + true => quote!(#core_types::Status::no_partial()), + false => quote!(#core_types::Status::new()), + }; - 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 { - #eval_prelude - self::#fn_name(__input #(, &self.#data_field_names)* #(, #regular_field_names)*) #await_keyword - }) + let lift = |call: TokenStream2| match tier { + NodeOutputTier::Plain | NodeOutputTier::Poll => quote!(Ok(#call)), + NodeOutputTier::Interrupt => call, + NodeOutputTier::Fallback => quote! { + match #call { + Ok(value) => Ok(value), + Err((value, error)) => { + __cell.fail(error); + Ok(value) + } + } + }, + }; + + let lift_slot = |call: TokenStream2| match tier { + NodeOutputTier::Plain | NodeOutputTier::Poll => call, + NodeOutputTier::Interrupt => quote!(#call?), + NodeOutputTier::Fallback => quote! { + match #call { + Ok(value) => value, + Err((value, error)) => { + __cell.fail(error); + value + } + } + }, + }; + + let fold = match tier { + NodeOutputTier::Poll => quote! { + match __kernel(&__cell) { + Ok(state) => __cell.merge(state), + Err(interrupt) => __cell.bail(interrupt), + } + }, + _ => quote!(__cell.run(__kernel)), + }; + + let kernel_call = lift(quote!(self::#fn_name(__input.clone() #(, &self.#data_field_names)* #(, #regular_field_names)*))); + + let eval_impl = if parsed.is_async { + let declared_field_names: Vec<_> = fields.iter().filter(|field| !field.is_data_field).map(|field| &field.pat_ident.ident).collect(); + let placeholder = match &attributes.placeholder { + Some(path) => quote!(Some(#path(#(&#declared_field_names),*))), + None => quote!(None), + }; + let data_clones = data_field_names.iter().map(|name| quote!(let #name = self.#name.clone();)); + quote! { + type Output = #output; + #[inline] + fn eval(&self, __input: &#input_type) -> #core_types::Poll { + let __cell = #cell_ctor; + let __kernel = |__cell: &#core_types::Status| -> Result<_, #core_types::Interrupt> { + #eval_prelude + Ok((#(#regular_field_names,)*)) + }; + let (#(#regular_field_names,)*) = match __kernel(&__cell) { + Ok(inputs) => inputs, + Err(interrupt) => return __cell.bail(interrupt), + }; + let __placeholder: Option = #placeholder; + if __cell.finality() == #core_types::Finality::Partial { + return match __placeholder { + Some(placeholder) => __cell.finish(placeholder), + None => #core_types::Poll::Pending, + }; + } + let __source = *_source.element(); + let __key = { + let mut __hasher = std::hash::DefaultHasher::new(); + #core_types::CacheHash::cache_hash(&#core_types::OwnedContextImpl::from(__input.clone()).without_source(__source), &mut __hasher); + std::hash::Hasher::finish(&__hasher) + }; + let __runtime = _runtime.element().clone(); + let __context = __input.clone(); + #(#data_clones)* + let __state = self.slot.resolve(__key, &__runtime, __source, __placeholder, move || async move { + #core_types::Poll::Final(self::#fn_name(__context #(, &#data_field_names)* #(, #regular_field_names)*).await) + }); + __cell.merge(__state) + } + + #serialize_impl } + } else { + quote! { + type Output = #output; + #[inline] + fn eval(&self, __input: &#input_type) -> #core_types::Poll { + let __cell = #cell_ctor; + let __kernel = |__cell: &#core_types::Status| -> Result<_, #core_types::Interrupt> { + #eval_prelude + #kernel_call + }; + #fold + } - #serialize_impl + #serialize_impl + } }; // The mapped variant zips every ranked connector by frame slot (longest-list, last-element repeats), broadcasting bare and environment parameters by clone @@ -584,31 +686,34 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn quote!(#core_types::list::List::with_capacity(__frame_length)), quote!(__output.push(__result);), ), - None => (quote!(#output_type), quote!(#core_types::list::List::new()), quote!(__output.extend(__result);)), + None => (quote!(#output), quote!(#core_types::list::List::new()), quote!(__output.extend(__result);)), }; + let slot_call = lift_slot(quote!(self::#fn_name(__slot_context #(, &self.#data_field_names)* #(, #per_slot_args)*))); quote! { - type Output = #core_types::registry::DynFuture<'n, #mapped_output_type>; + type Output = #mapped_output_type; #[inline] #[allow(clippy::clone_on_copy)] - fn eval(&'n self, __input: #input_type) -> Self::Output { - Box::pin(async move { + fn eval(&self, __input: &#input_type) -> #core_types::Poll { + let __cell = #cell_ctor; + let __kernel = |__cell: &#core_types::Status| -> Result<_, #core_types::Interrupt> { #eval_prelude let __frame_length = [#(#ranked_names.len()),*].into_iter().max().unwrap_or(0); if [#(#ranked_names.len()),*].into_iter().any(|length| length == 0) { - return #core_types::list::List::new(); + return Ok(#core_types::list::List::new()); } let mut __output = #initial_output; for __slot_index in 0..__frame_length { let __slot_context = #slot_context; - let __result = self::#fn_name(__slot_context #(, &self.#data_field_names)* #(, #per_slot_args)*) #await_keyword; + let __result = #slot_call; #collect_result } - __output - }) + Ok(__output) + }; + __cell.run(__kernel) } #serialize_impl @@ -633,7 +738,10 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn .map(|(index, field)| { let name = &field.pat_ident.ident; if index == primary_index { - return quote!(&__stub); + return match tier { + NodeOutputTier::Poll => quote!(&__stub), + _ => quote!(#core_types::LazyInput::new(&__stub, __cell, #primary_index)), + }; } match &field.ty { ParsedFieldType::Node(_) => quote!(#name), @@ -651,26 +759,28 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn quote!(#core_types::list::List::with_capacity(__frame_length)), quote!(__output.push(__result);), ), - None => (quote!(#output_type), quote!(#core_types::list::List::new()), quote!(__output.extend(__result);)), + None => (quote!(#output), quote!(#core_types::list::List::new()), quote!(__output.extend(__result);)), }; let empty_param_check = (!ranked_names.is_empty()).then(|| { quote! { if [#(#ranked_names.len()),*].into_iter().any(|__length| __length == 0) { - return #core_types::list::List::new(); + return Ok(#core_types::list::List::new()); } } }); + let slot_call = lift_slot(quote!(self::#fn_name(__input.clone() #(, &self.#data_field_names)* #(, #per_slot_args)*))); quote! { - type Output = #core_types::registry::DynFuture<'n, #list_content_output_type>; + type Output = #list_content_output_type; #[inline] #[allow(clippy::clone_on_copy)] - fn eval(&'n self, __input: #input_type) -> Self::Output { - Box::pin(async move { + fn eval(&self, __input: &#input_type) -> #core_types::Poll { + let __cell = #cell_ctor; + let __kernel = |__cell: &#core_types::Status| -> Result<_, #core_types::Interrupt> { #eval_prelude - let __content = #primary_name.eval(#core_types::OwnedContextImpl::from(__input.clone()).into_context()) #await_keyword; + let __content = #primary_name.eval(#core_types::OwnedContextImpl::from(__input.clone()).into_context())?; let __frame_length = __content.len(); #empty_param_check @@ -679,12 +789,13 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let __stub = #core_types::value::PrecomputedItemNode::new( __content.clone_item(__slot_index).expect("A content slot index is always within bounds") ); - let __result = self::#fn_name(__input.clone() #(, &self.#data_field_names)* #(, #per_slot_args)*) #await_keyword; + let __result = #slot_call; #collect_result } - __output - }) + Ok(__output) + }; + __cell.run(__kernel) } #serialize_impl @@ -718,7 +829,7 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn (Some(mapped_where_clause), Some(mapped_eval)) => quote! { #cfg #[automatically_derived] - impl<'n, #(#fn_generics,)* #(#node_generics,)* #(#future_idents,)*> #core_types::Node<'n, #input_type> for #mod_name::#mapped_struct_name<#(#struct_type_params,)*> + impl<#(#fn_generics,)* #(#node_generics,)*> #core_types::Node<#input_type> for #mod_name::#mapped_struct_name<#(#struct_type_params,)*> #mapped_where_clause { #mapped_eval @@ -731,7 +842,7 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn (Some(list_content_where_clause), Some(list_content_eval)) => quote! { #cfg #[automatically_derived] - impl<'n, #(#fn_generics,)* #(#node_generics,)* #(#future_idents,)*> #core_types::Node<'n, #input_type> for #mod_name::#list_content_struct_name<#(#struct_type_params,)*> + impl<#(#fn_generics,)* #(#node_generics,)*> #core_types::Node<#input_type> for #mod_name::#list_content_struct_name<#(#struct_type_params,)*> #list_content_where_clause { #list_content_eval @@ -748,7 +859,7 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn } #[automatically_derived] - impl<'n, #(#struct_generic_params,)*> #mapped_struct_name<#(#struct_type_params,)*> + impl<#(#struct_generic_params,)*> #mapped_struct_name<#(#struct_type_params,)*> { #[allow(clippy::too_many_arguments)] pub fn new(#(#new_args,)*) -> Self { @@ -768,7 +879,7 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn } #[automatically_derived] - impl<'n, #(#struct_generic_params,)*> #list_content_struct_name<#(#struct_type_params,)*> + impl<#(#struct_generic_params,)*> #list_content_struct_name<#(#struct_type_params,)*> { #[allow(clippy::too_many_arguments)] pub fn new(#(#new_args,)*) -> Self { @@ -825,7 +936,7 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn #(#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 + #vis #async_keyword fn #fn_name <#(#fn_generics,)*> (#input_ident: #input_type #(, #data_field_idents: #data_field_types)* #(, #field_idents: #field_types)*) -> #output_type #where_clause #body } }; // A shader node's fn is emitted twice from the one body: as declared, in f64, for the CPU, and with the f32 parameter @@ -849,7 +960,7 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn #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,)*> + impl<#(#fn_generics,)* #(#node_generics,)*> #core_types::Node<#input_type> for #mod_name::#struct_name<#(#struct_type_params,)*> #struct_where_clause { #eval_impl @@ -884,7 +995,7 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn 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::registry::{NodeMetadata, FieldMetadata, NODE_REGISTRY, NODE_METADATA, DynAnyNode, DowncastBothNode, TypeErasedBox, PanicNode, RegistryValueSource, RegistryWidgetOverride}; use gcore::ctor::ctor; // Use the types specified in the implementation @@ -897,7 +1008,7 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn } #[automatically_derived] - impl<'n, #(#struct_generic_params,)*> #struct_name<#(#struct_type_params,)*> + impl<#(#struct_generic_params,)*> #struct_name<#(#struct_type_params,)*> { #[allow(clippy::too_many_arguments)] pub fn new(#(#new_args,)*) -> Self { @@ -1229,17 +1340,12 @@ fn generate_register_node_impl( }; let signature_type = signature_type.unwrap_or_else(|| quote!(#output_type)); - 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, #signature_type))); - panic_node_types.push(quote!(#input_type, DynFuture<'static, #output_type>)); + panic_node_types.push(quote!(#input_type, #output_type)); } let input_type = match parsed.input.implementations.is_empty() { true => parsed.input.ty.clone(), @@ -1260,8 +1366,7 @@ fn generate_register_node_impl( }, { let node = #variant_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 + NodeIO::<#input_type>::to_async_node_io(&node, params) } ) )); diff --git a/node-graph/node-macro/src/parsing.rs b/node-graph/node-macro/src/parsing.rs index 2d4a82f9658..d44cf152834 100644 --- a/node-graph/node-macro/src/parsing.rs +++ b/node-graph/node-macro/src/parsing.rs @@ -58,6 +58,10 @@ pub(crate) struct NodeFnAttributes { pub(crate) memoize: bool, /// Whether this node provides a scope pub(crate) inject_scope: bool, + /// Fn producing an placeholder value used while node evaluation is pending + pub(crate) placeholder: Option, + /// Whether the node refuses placeholder data + pub(crate) no_partial: bool, } #[derive(Clone, Debug, Default)] @@ -448,6 +452,8 @@ impl Parse for NodeFnAttributes { let mut serialize = None; let mut memoize = false; let mut inject_scope = false; + let mut placeholder = None; + let mut no_partial = false; let content = input; // let content; @@ -590,13 +596,37 @@ impl Parse for NodeFnAttributes { } inject_scope = true; } + // The placeholder value used while a node's evaluation is pending. + // + // Example usage: + // #[node_macro::node(..., placeholder = get_placeholder, ...)] + "placeholder" => { + let meta = meta.require_name_value()?; + if placeholder.is_some() { + return Err(Error::new_spanned(meta, "Multiple 'placeholder' attributes are not allowed")); + } + let parsed_path: Path = + syn::parse2(meta.value.to_token_stream()).map_err(|_| Error::new_spanned(meta, "Expected a fn path for 'placeholder', e.g., placeholder = get_placeholder"))?; + placeholder = Some(parsed_path); + } + // Maps `Partial` inputs to `Pending`, for nodes where placeholder data makes no sense. + // + // 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', and 'no_partial'. Example usage: #[node_macro::node(..., name("Test Node"), ...)] "# @@ -630,6 +660,8 @@ impl Parse for NodeFnAttributes { serialize, memoize, inject_scope, + placeholder, + no_partial, }) } } @@ -1164,7 +1196,7 @@ impl ParsedNodeFn { if let Type::ImplTrait(impl_trait) = self.input.ty.clone() { let ident = Ident::new("_Input", impl_trait.span()); let mut bounds = impl_trait.bounds; - bounds.push(parse_quote!('n)); + bounds.push(parse_quote!(Clone)); self.fn_generics.push(GenericParam::Type(TypeParam { attrs: Default::default(), ident: ident.clone(), @@ -1348,6 +1380,8 @@ mod tests { serialize: None, memoize: false, inject_scope: false, + placeholder: None, + no_partial: false, }, fn_name: Ident::new("add", Span::call_site()), struct_name: Ident::new("Add", Span::call_site()), @@ -1422,6 +1456,8 @@ mod tests { serialize: None, memoize: false, inject_scope: false, + placeholder: None, + no_partial: false, }, fn_name: Ident::new("transform", Span::call_site()), struct_name: Ident::new("Transform", Span::call_site()), @@ -1513,6 +1549,8 @@ mod tests { serialize: None, memoize: false, inject_scope: false, + placeholder: None, + no_partial: false, }, fn_name: Ident::new("circle", Span::call_site()), struct_name: Ident::new("Circle", Span::call_site()), @@ -1583,6 +1621,8 @@ mod tests { serialize: None, memoize: false, inject_scope: false, + placeholder: None, + no_partial: false, }, fn_name: Ident::new("levels", Span::call_site()), struct_name: Ident::new("Levels", Span::call_site()), @@ -1665,6 +1705,8 @@ mod tests { serialize: None, memoize: false, inject_scope: false, + placeholder: None, + no_partial: false, }, fn_name: Ident::new("add", Span::call_site()), struct_name: Ident::new("Add", Span::call_site()), @@ -1750,6 +1792,8 @@ mod tests { serialize: None, memoize: false, inject_scope: false, + placeholder: None, + no_partial: false, }, fn_name: Ident::new("load_image", Span::call_site()), struct_name: Ident::new("LoadImage", Span::call_site()), @@ -1820,6 +1864,8 @@ mod tests { serialize: None, memoize: false, inject_scope: false, + placeholder: 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/validation.rs b/node-graph/node-macro/src/validation.rs index 5d432c57533..67d53a75960 100644 --- a/node-graph/node-macro/src/validation.rs +++ b/node-graph/node-macro/src/validation.rs @@ -72,6 +72,12 @@ fn validate_no_item_parameters(parsed: &ParsedNodeFn) { } fn validate_element_wise(parsed: &ParsedNodeFn) { + if parsed.is_async && parsed.fields.iter().any(|field| matches!(field.ty, ParsedFieldType::Node(_))) { + emit_error!(parsed.fn_name.span(), "An async node cannot take `impl Node` inputs"); + } + if parsed.attributes.placeholder.is_some() && !parsed.is_async { + emit_error!(parsed.fn_name.span(), "Only an async node can have a `placeholder`"); + } if parsed.attributes.skip_impl { return; } diff --git a/node-graph/nodes/gcore/src/animation.rs b/node-graph/nodes/gcore/src/animation.rs index 2f42fcd933d..8ed8c6b187b 100644 --- a/node-graph/nodes/gcore/src/animation.rs +++ b/node-graph/nodes/gcore/src/animation.rs @@ -1,6 +1,6 @@ use core_types::list::{Item, List}; use core_types::transform::Footprint; -use core_types::{CacheHash, CloneVarArgs, Color, Context, Ctx, ExtractAll, ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime, OwnedContextImpl}; +use core_types::{CacheHash, CloneVarArgs, Color, Context, Ctx, ExtractAll, ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime, Interrupt, OwnedContextImpl}; use glam::{DAffine2, DVec2}; use graphic_types::vector_types::Gradient; use graphic_types::{Artboard, Graphic, Vector}; @@ -64,7 +64,7 @@ fn animation_time( } #[node_macro::node(category("Debug"))] -async fn quantize_real_time( +fn quantize_real_time( ctx: impl Ctx + ExtractAll + CloneVarArgs, #[implementations( Context -> Item, @@ -92,11 +92,11 @@ async fn quantize_real_time( Context -> List, Context -> List, )] - value: impl Node<'n, Context<'static>, Output = T>, + value: impl Node, Output = T>, #[default(1)] #[unit(" sec")] quantum: Item, -) -> T { +) -> Result { let time = ctx.try_real_time().unwrap_or_default(); let time = time / 1000.; let quantum = quantum.into_element(); @@ -106,11 +106,11 @@ async fn quantize_real_time( } let quantized_time = quantized_time * 1000.; let new_context = OwnedContextImpl::from(ctx).with_real_time(quantized_time); - value.eval(Some(new_context.into())).await + value.eval(Some(new_context.into())) } #[node_macro::node(category("Debug"))] -async fn quantize_animation_time( +fn quantize_animation_time( ctx: impl Ctx + ExtractAll + CloneVarArgs, #[implementations( Context -> Item, @@ -138,11 +138,11 @@ async fn quantize_animation_time( Context -> List, Context -> List, )] - value: impl Node<'n, Context<'static>, Output = T>, + value: impl Node, Output = T>, #[default(1)] #[unit(" sec")] quantum: Item, -) -> T { +) -> Result { let time = ctx.try_animation_time().unwrap_or_default(); let quantum = quantum.into_element(); let mut quantized_time = (time * quantum.recip()).round() / quantum.recip(); @@ -150,7 +150,7 @@ async fn quantize_animation_time( quantized_time = time; } let new_context = OwnedContextImpl::from(ctx).with_animation_time(quantized_time); - value.eval(Some(new_context.into())).await + value.eval(Some(new_context.into())) } /// Produces the current position of the user's pointer within the document canvas. diff --git a/node-graph/nodes/gcore/src/context_modification.rs b/node-graph/nodes/gcore/src/context_modification.rs index 0e759dd955b..959d0f4ae63 100644 --- a/node-graph/nodes/gcore/src/context_modification.rs +++ b/node-graph/nodes/gcore/src/context_modification.rs @@ -2,7 +2,7 @@ use core::f64; use core_types::context::{CloneVarArgs, Context, ContextModification, Ctx, ExtractAll}; use core_types::list::{AttributeValueDyn, Item, List, ListDyn, NodeIdPath}; use core_types::transform::Footprint; -use core_types::{Color, OwnedContextImpl}; +use core_types::{Color, Interrupt, OwnedContextImpl}; use glam::{DAffine2, DVec2}; use graphic_types::vector_types::Gradient; use graphic_types::{Artboard, Graphic, Vector}; @@ -11,7 +11,7 @@ use raster_types::{CPU, GPU, Raster}; /// Filters out what should be unused components of the context based on the specified requirements. /// This node is inserted by the compiler to "zero out" unused context components. #[node_macro::node(category(""))] -async fn context_modification( +fn context_modification( ctx: impl Ctx + CloneVarArgs + ExtractAll, /// The data to pass through, evaluated with the stripped down context. #[implementations( @@ -46,10 +46,10 @@ async fn context_modification( value: impl Node, Output = T>, /// The parts of the context to keep when evaluating the input value. All other parts are nullified. modification: Item, -) -> T { +) -> Result { let new_context = OwnedContextImpl::from_modification(ctx, modification.element()); - value.eval(Some(new_context.into())).await + value.eval(Some(new_context.into())) } #[cfg(test)] diff --git a/node-graph/nodes/gcore/src/memo.rs b/node-graph/nodes/gcore/src/memo.rs index e51c3522e16..4ae8c9cd588 100644 --- a/node-graph/nodes/gcore/src/memo.rs +++ b/node-graph/nodes/gcore/src/memo.rs @@ -1,6 +1,6 @@ -use core_types::WasmNotSend; use core_types::graphene_hash::CacheHash; use core_types::memo::*; +use core_types::{Interrupt, Poll, WasmNotSend}; use std::hash::DefaultHasher; use std::hash::Hasher; use std::sync::Arc; @@ -10,7 +10,7 @@ use std::sync::Mutex; /// /// Stores the last evaluated data that flowed through this node and immediately returns that data on subsequent renders if the context has not changed. #[node_macro::node(category("General"), path(graphene_core::memo), skip_impl)] -async fn memoize(input: I, content: impl Node, #[data] cache: Arc>>) -> T { +fn memoize(input: I, content: impl Node, #[data] cache: Arc)>>>) -> Poll { // Caches the output of a given node called with a specific input. // // A cache miss occurs when the Option is None. In this case, the node evaluates the inner node and memoizes (stores) the result. @@ -27,25 +27,27 @@ async fn memoize(input: I, con return data; } - let value = content.eval(input).await; - *cache.lock().unwrap() = Some((hash, value.clone())); - value + let state = content.eval(&input); + if matches!(state, Poll::Final(_) | Poll::Partial(_)) { + *cache.lock().unwrap() = Some((hash, state.clone())); + } + state } type MonitorValue = Arc>>>>; /// The Monitor node is used by the editor to access the data flowing through it. #[node_macro::node(category(""), path(graphene_core::memo), serialize(serialize_monitor), properties("monitor_properties"), skip_impl)] -async fn monitor( +fn monitor( input: I, content: impl Node, #[allow(clippy::type_complexity)] #[data] io: MonitorValue, -) -> T { - let output = content.eval(input.clone()).await; +) -> Result { + let output = content.eval(input.clone())?; *io.lock().unwrap() = Some(Arc::new(IORecord { input, output: output.clone() })); - output + Ok(output) } fn serialize_monitor(io: &MonitorValue) -> Option> { diff --git a/node-graph/nodes/gstd/src/any.rs b/node-graph/nodes/gstd/src/any.rs index c8959d23e2f..6aa4eced61b 100644 --- a/node-graph/nodes/gstd/src/any.rs +++ b/node-graph/nodes/gstd/src/any.rs @@ -1,10 +1,8 @@ -use core_types::NodeIO; use core_types::WasmNotSend; -pub use core_types::registry::{DowncastBothNode, DynAnyNode, FutureWrapperNode, PanicNode}; +pub use core_types::registry::{DowncastBothNode, DynAnyNode, PanicNode}; pub use core_types::{Node, generic, ops}; -use dyn_any::StaticType; -pub use graph_craft::proto::{Any, NodeContainer, TypeErasedBox, TypeErasedNode}; -use graph_craft::proto::{FutureAny, SharedNodeContainer}; +use graph_craft::proto::SharedNodeContainer; +pub use graph_craft::proto::{Any, AnyInput, NodeContainer, TypeErasedBox, TypeErasedNode}; pub trait IntoTypeErasedNode<'n> { fn into_type_erased(self) -> TypeErasedBox<'n>; @@ -12,16 +10,16 @@ pub trait IntoTypeErasedNode<'n> { impl<'n, N: 'n> IntoTypeErasedNode<'n> for N where - N: for<'i> NodeIO<'i, Any<'i>, Output = FutureAny<'i>> + Sync + WasmNotSend, + N: Node + Sync + WasmNotSend, { fn into_type_erased(self) -> TypeErasedBox<'n> { Box::new(self) } } -pub fn input_node(n: SharedNodeContainer) -> DowncastBothNode<(), O> { +pub fn input_node(n: SharedNodeContainer) -> DowncastBothNode<(), O> { downcast_node(n) } -pub fn downcast_node(n: SharedNodeContainer) -> DowncastBothNode { +pub fn downcast_node(n: SharedNodeContainer) -> DowncastBothNode { DowncastBothNode::new(n) } diff --git a/node-graph/nodes/gstd/src/platform_application_io.rs b/node-graph/nodes/gstd/src/platform_application_io.rs index 6bcaa7af841..df81c215727 100644 --- a/node-graph/nodes/gstd/src/platform_application_io.rs +++ b/node-graph/nodes/gstd/src/platform_application_io.rs @@ -41,7 +41,7 @@ fn parse_headers(headers: &str) -> reqwest::header::HeaderMap { } /// Sends an HTTP GET request to a specified URL and optionally waits for the response (unless discarded) which is output as a string. -#[node_macro::node(category("Web Request"))] +#[node_macro::node(category("Web Request"), placeholder = pending_text)] async fn get_request( _: impl Ctx, _primary: (), @@ -59,14 +59,7 @@ async fn get_request( let request = reqwest::Client::new().get(url).headers(header_map); if discard_result { - #[cfg(target_family = "wasm")] - wasm_bindgen_futures::spawn_local(async move { - let _ = request.send().await; - }); - #[cfg(all(not(target_family = "wasm"), feature = "tokio"))] - tokio::spawn(async move { - let _ = request.send().await; - }); + let _ = request.send().await; return Item::default(); } @@ -76,8 +69,12 @@ async fn get_request( Item::new_from_element(response.text().await.ok().unwrap_or_default()) } +fn pending_text(_primary: &(), _url: &Item, _discard_result: &Item, _headers: &Item) -> Item { + Item::default() +} + /// Sends an HTTP POST request to a specified URL with the provided binary data and optionally waits for the response (unless discarded) which is output as a string. -#[node_macro::node(category("Web Request"))] +#[node_macro::node(category("Web Request"), placeholder = pending_post_text)] async fn post_request( _: impl Ctx, _primary: (), @@ -99,14 +96,7 @@ async fn post_request( let request = reqwest::Client::new().post(url).body(body_bytes).headers(header_map); if discard_result { - #[cfg(target_family = "wasm")] - wasm_bindgen_futures::spawn_local(async move { - let _ = request.send().await; - }); - #[cfg(all(not(target_family = "wasm"), feature = "tokio"))] - tokio::spawn(async move { - let _ = request.send().await; - }); + let _ = request.send().await; return Item::default(); } @@ -116,6 +106,10 @@ async fn post_request( Item::new_from_element(response.text().await.ok().unwrap_or_default()) } +fn pending_post_text(_primary: &(), _url: &Item, _body: &Item, _discard_result: &Item, _headers: &Item) -> Item { + Item::default() +} + /// Converts a text string to raw binary data. Useful for transmission over HTTP or writing to files. #[node_macro::node(category("Web Request"), name("String to Bytes"))] fn string_to_bytes(_: impl Ctx, string: Item) -> Item { @@ -139,7 +133,7 @@ fn image_to_bytes(_: impl Ctx, image: Item>) -> Item { } /// Loads binary from URLs and local asset paths. Returns a transparent placeholder if the resource fails to load, allowing rendering to continue. -#[node_macro::node(category("Web Request"))] +#[node_macro::node(category("Web Request"), placeholder = pending_resource)] async fn load_resource(_: impl Ctx, _primary: (), #[name("URL")] url: Item) -> Item { let url = url.into_element(); let placeholder = || -> Item { Item::new_from_element(Resource::empty()) }; @@ -161,6 +155,10 @@ async fn load_resource(_: impl Ctx, _primary: (), #[name("URL")] url: Item) -> Item { + Item::new_from_element(Resource::empty()) +} + /// Converts the raw byte data of an image file to a raster image. /// /// Supports the formats: PNG, JPG, GIF, WEBP, TIFF, BMP, TGA, ICO, HDR, EXR. @@ -180,7 +178,7 @@ fn create_canvas(_: impl Ctx) -> Item { /// Renders a view of the input graphic within an area defined by the *Footprint*. #[cfg(target_family = "wasm")] -#[node_macro::node(category(""))] +#[node_macro::node(category(""), placeholder = pending_raster)] async fn rasterize(_: impl Ctx, data: List, footprint: Item, canvas: Item) -> List> { let mut data = data; let mut canvas = canvas.into_element(); @@ -239,12 +237,17 @@ async fn rasterize(_: impl Ctx, data: List, footprint: Item, ) } +#[cfg(target_family = "wasm")] +fn pending_raster(_data: &List, _footprint: &Item, _canvas: &Item) -> List> { + List::new() +} + #[node_macro::node(category(""), inject_scope)] fn editor_api(_: impl Ctx, #[scope("editor-api")] editor_api: Item>) -> Item> { editor_api } -#[node_macro::node(category(""))] +#[node_macro::node(category(""), placeholder = pending_stored_resource)] async fn resource( _: impl Ctx, /// The scope-provided editor API giving access to the platform's resource storage. @@ -270,6 +273,10 @@ async fn resource( Item::new_from_element(resource) } +fn pending_stored_resource(_editor_api: &Item>, _hash: &Item) -> Item { + Item::new_from_element(Resource::empty()) +} + #[node_macro::node(category(""), inject_scope)] fn wgpu_executor(_: impl Ctx, #[scope(editor_api::IDENTIFIER)] editor_api: Item>) -> Item<::wgpu_executor::WgpuExecutor> { let executor = editor_api From a1b33d5fca6dbbd36f42eddf21d10d68e7a276dc Mon Sep 17 00:00:00 2001 From: Timon Date: Fri, 9 Oct 2026 19:43:55 +0000 Subject: [PATCH 27/54] Make lazy input nodes, tests and benches sync --- node-graph/graphene-cli/src/export.rs | 10 +- .../benches/run_cached.rs | 4 +- .../benches/run_cached_gungraun.rs | 4 +- .../benches/run_demo_art_criterion.rs | 2 +- .../interpreted-executor/benches/run_once.rs | 2 +- .../benches/run_once_gungraun.rs | 2 +- .../src/dynamic_executor/test.rs | 60 +++--- node-graph/nodes/graphic/src/artboard.rs | 12 +- node-graph/nodes/graphic/src/graphic.rs | 28 +-- node-graph/nodes/gstd/src/render_cache.rs | 31 ++- node-graph/nodes/gstd/src/render_node.rs | 18 +- .../nodes/gstd/src/render_pixel_preview.rs | 16 +- node-graph/nodes/math/src/lib.rs | 12 +- node-graph/nodes/raster/src/blending_nodes.rs | 4 +- node-graph/nodes/repeat/src/repeat_nodes.rs | 135 ++++++------- node-graph/nodes/text/src/lib.rs | 9 +- .../nodes/transform/src/transform_nodes.rs | 20 +- .../vector/src/vector_modification_nodes.rs | 2 +- node-graph/nodes/vector/src/vector_nodes.rs | 186 +++++++++--------- 19 files changed, 277 insertions(+), 280 deletions(-) diff --git a/node-graph/graphene-cli/src/export.rs b/node-graph/graphene-cli/src/export.rs index 83d2392e46b..23458e89347 100644 --- a/node-graph/graphene-cli/src/export.rs +++ b/node-graph/graphene-cli/src/export.rs @@ -60,7 +60,10 @@ pub async fn export_document( } // Execute the graph - let result = executor.execute(render_config.into_context_with_generations(generations)).await?; + let result = executor + .execute(render_config.into_context_with_generations(generations))? + .value() + .ok_or("The node graph produced no frame")?; // Handle the result based on output type match result { @@ -199,7 +202,10 @@ pub async fn export_gif( } // Execute the graph for this frame - let result = executor.execute(render_config.into_context_with_generations(generations.clone())).await?; + let result = executor + .execute(render_config.into_context_with_generations(generations.clone()))? + .value() + .ok_or("The node graph produced no frame")?; // Extract RGBA data from result let (data, img_width, img_height) = match result { diff --git a/node-graph/interpreted-executor/benches/run_cached.rs b/node-graph/interpreted-executor/benches/run_cached.rs index 621a0c1df26..df9692aa0e7 100644 --- a/node-graph/interpreted-executor/benches/run_cached.rs +++ b/node-graph/interpreted-executor/benches/run_cached.rs @@ -10,9 +10,7 @@ fn subsequent_evaluations(c: &mut Criterion) { bench_for_each_demo(&mut group, |name, g| { let (executor, _) = setup_network(name); let context = context.clone(); - g.bench_function(name, |b| { - b.iter(|| futures::executor::block_on(executor.tree().eval_tagged_value(executor.output(), std::hint::black_box(context.clone()))).unwrap()) - }); + g.bench_function(name, |b| b.iter(|| executor.tree().eval_tagged_value(executor.output(), std::hint::black_box(&context)).unwrap())); }); group.finish(); } diff --git a/node-graph/interpreted-executor/benches/run_cached_gungraun.rs b/node-graph/interpreted-executor/benches/run_cached_gungraun.rs index a9835256697..ce81e8eb74f 100644 --- a/node-graph/interpreted-executor/benches/run_cached_gungraun.rs +++ b/node-graph/interpreted-executor/benches/run_cached_gungraun.rs @@ -11,7 +11,7 @@ fn setup_run_cached(name: &str) -> DynamicExecutor { // Warm up the cache by running once let context = RenderConfig::default().into_context(); - let _ = futures::executor::block_on(executor.tree().eval_tagged_value(executor.output(), context)); + let _ = executor.tree().eval_tagged_value(executor.output(), &context); executor } @@ -20,7 +20,7 @@ fn setup_run_cached(name: &str) -> DynamicExecutor { #[benches::with_setup(args = ["isometric-fountain", "painted-dreams", "parametric-dunescape", "red-dress", "valley-of-spires"], setup = setup_run_cached)] pub fn run_cached(executor: DynamicExecutor) -> DynamicExecutor { let context = RenderConfig::default().into_context(); - black_box(futures::executor::block_on(executor.tree().eval_tagged_value(executor.output(), black_box(context))).unwrap()); + black_box(executor.tree().eval_tagged_value(executor.output(), black_box(&context)).unwrap()); // Return the executor so its teardown happens outside the measured section executor diff --git a/node-graph/interpreted-executor/benches/run_demo_art_criterion.rs b/node-graph/interpreted-executor/benches/run_demo_art_criterion.rs index 8334b017bc7..048abcbda32 100644 --- a/node-graph/interpreted-executor/benches/run_demo_art_criterion.rs +++ b/node-graph/interpreted-executor/benches/run_demo_art_criterion.rs @@ -36,7 +36,7 @@ fn run_once(name: &str, c: &mut BenchmarkGroup) { let executor = DynamicExecutor::new(proto_network).unwrap(); let footprint = Footprint::default(); - c.bench_function(name, |b| b.iter(|| futures::executor::block_on((&executor).execute(footprint)))); + c.bench_function(name, |b| b.iter(|| (&executor).execute(footprint))); } fn run_once_demo(c: &mut Criterion) { let mut g = c.benchmark_group("Run Once no render"); diff --git a/node-graph/interpreted-executor/benches/run_once.rs b/node-graph/interpreted-executor/benches/run_once.rs index f63ca9df436..693cc11885d 100644 --- a/node-graph/interpreted-executor/benches/run_once.rs +++ b/node-graph/interpreted-executor/benches/run_once.rs @@ -12,7 +12,7 @@ fn run_once(c: &mut Criterion) { g.bench_function(name, |b| { b.iter_batched( || setup_network(name), - |(executor, _)| futures::executor::block_on(executor.tree().eval_tagged_value(executor.output(), std::hint::black_box(context.clone()))).unwrap(), + |(executor, _)| executor.tree().eval_tagged_value(executor.output(), std::hint::black_box(&context)).unwrap(), criterion::BatchSize::SmallInput, ) }); diff --git a/node-graph/interpreted-executor/benches/run_once_gungraun.rs b/node-graph/interpreted-executor/benches/run_once_gungraun.rs index 307dbb64fa1..a7489393015 100644 --- a/node-graph/interpreted-executor/benches/run_once_gungraun.rs +++ b/node-graph/interpreted-executor/benches/run_once_gungraun.rs @@ -15,7 +15,7 @@ fn setup_run_once(name: &str) -> DynamicExecutor { #[benches::with_setup(args = ["isometric-fountain", "painted-dreams", "procedural-string-lights", "parametric-dunescape", "red-dress", "valley-of-spires"], setup = setup_run_once)] pub fn run_once(executor: DynamicExecutor) -> DynamicExecutor { let context = application_io::RenderConfig::default().into_context(); - black_box(futures::executor::block_on(executor.tree().eval_tagged_value(executor.output(), black_box(context))).unwrap()); + black_box(executor.tree().eval_tagged_value(executor.output(), black_box(&context)).unwrap()); // Return the executor so its teardown happens outside the measured section executor diff --git a/node-graph/interpreted-executor/src/dynamic_executor/test.rs b/node-graph/interpreted-executor/src/dynamic_executor/test.rs index 4dc309a72d0..58774520cef 100644 --- a/node-graph/interpreted-executor/src/dynamic_executor/test.rs +++ b/node-graph/interpreted-executor/src/dynamic_executor/test.rs @@ -13,7 +13,7 @@ fn push_node_sync() { let context = TypingContext::default(); tree.push_node(NodeId(0), val_1_protonode, &context).unwrap(); let _node = tree.get(NodeId(0)).unwrap(); - let result: Option> = futures::executor::block_on(tree.eval(NodeId(0), ())); + let result: Option> = tree.eval(NodeId(0), &()); assert_eq!(result.map(|item| *item.element()), Some(2_i64)); } @@ -43,10 +43,10 @@ fn item_wire_variant_resolves_and_executes() { let tree = compile_bounding_box_network(TaggedValue::TypeDefault(item!(Vector))); let context: Context = None; - let result: Option> = futures::executor::block_on(tree.eval(NodeId(1), context.clone())); + let result: Option> = tree.eval(NodeId(1), &context); assert!(result.is_some(), "The Item wire variant should downcast and execute end-to-end"); - let wrong_type: Option> = futures::executor::block_on(tree.eval(NodeId(1), context)); + let wrong_type: Option> = tree.eval(NodeId(1), &context); assert!(wrong_type.is_none(), "An Item wire should not downcast as a List"); } @@ -69,7 +69,7 @@ fn item_wire_promotes_to_list_connector() { let tree = BorrowTree::new(network, &typing_context).expect("The promotion adapter should instantiate"); let context: Context = None; - let result: Option> = futures::executor::block_on(tree.eval(NodeId(1), context)); + let result: Option> = tree.eval(NodeId(1), &context); assert!(result.is_some(), "The promoted wire should execute end-to-end"); } @@ -99,7 +99,7 @@ fn rank_0_content_promotes_through_the_layer_coercion_path() { let tree = BorrowTree::new(network, &typing_context).expect("The promotion adapters should instantiate"); let context: Context = None; - let result: Option> = futures::executor::block_on(tree.eval(NodeId(3), context)); + let result: Option> = tree.eval(NodeId(3), &context); let stack = result.expect("The layer coercion path should execute end-to-end"); assert_eq!(stack.len(), 1, "The rank-0 content should contribute exactly one graphic to the stack"); } @@ -133,7 +133,7 @@ fn mixed_rank_connectors_resolve_via_promotion() { let tree = BorrowTree::new(network, &typing_context).expect("Construction should wrap the promoted argument"); let context: Context = None; - let result: Option> = futures::executor::block_on(tree.eval(NodeId(3), context)); + let result: Option> = tree.eval(NodeId(3), &context); assert!(result.is_some(), "The zipped mapped variant should execute end-to-end"); } @@ -146,7 +146,7 @@ fn all_item_connectors_resolve_without_promotion() { let tree = BorrowTree::new(network, &typing_context).expect("The rank-0 variant should instantiate"); let context: Context = None; - let result: Option> = futures::executor::block_on(tree.eval(NodeId(3), context)); + let result: Option> = tree.eval(NodeId(3), &context); assert!(result.is_some(), "The rank-0 variant should execute and stay rank 0"); } @@ -194,7 +194,7 @@ fn transform_composes_onto_item_wire() { let tree = BorrowTree::new(network, &typing_context).expect("Transform's rank-0 variant should instantiate"); let context: Context = None; - let result: Option> = futures::executor::block_on(tree.eval(output, context)); + let result: Option> = tree.eval(output, &context); let item = result.expect("A rank-0 chain through Transform should stay rank 0"); let transform = item.attribute_cloned_or_default::(core_types::ATTR_TRANSFORM); assert_eq!(transform.translation, DVec2::new(5., 0.), "The translation should compose onto the item's transform attribute"); @@ -214,7 +214,7 @@ fn transform_broadcasts_item_content_across_a_framed_parameter() { let tree = BorrowTree::new(network, &typing_context).expect("The mapped variant should instantiate"); let context: Context = None; - let result: Option> = futures::executor::block_on(tree.eval(output, context)); + let result: Option> = tree.eval(output, &context); let list = result.expect("The broadcast should produce a List"); assert_eq!(list.len(), 2, "One output item per frame slot"); @@ -247,7 +247,7 @@ fn generator_frames_over_a_list_parameter() { let tree = BorrowTree::new(network, &typing_context).expect("The mapped generator variant should instantiate"); let context: Context = None; - let result: Option> = futures::executor::block_on(tree.eval(NodeId(3), context)); + let result: Option> = tree.eval(NodeId(3), &context); let list = result.expect("The generator frame should produce a List"); assert_eq!(list.len(), 3, "One circle per radius slot"); } @@ -317,7 +317,7 @@ fn value_wires_materialize_as_items_at_resolution() { let tree = BorrowTree::new(network, &typing_context).expect("The all-Item variant should instantiate"); let context: Context = None; - let result: Option> = futures::executor::block_on(tree.eval(NodeId(5), context)); + let result: Option> = tree.eval(NodeId(5), &context); let item = result.expect("A value matrix should flow through Transform as an Item"); let transform = item.attribute_cloned_or_default::(core_types::ATTR_TRANSFORM); assert_eq!(transform.translation, DVec2::new(7., 0.), "The translation should compose onto the gained transform attribute"); @@ -341,7 +341,7 @@ fn position_value_converts_through_the_vector_input_adapter() { let tree = BorrowTree::new(network, &typing_context).expect("The conversion constructor should instantiate"); let context: Context = None; - let result: Option> = futures::executor::block_on(tree.eval(NodeId(1), context)); + let result: Option> = tree.eval(NodeId(1), &context); assert!(result.is_some(), "The position should arrive as an Item single-anchor path"); } @@ -366,7 +366,7 @@ fn position_value_raises_into_the_into_group_reducer() { let tree = BorrowTree::new(network, &typing_context).expect("The reducer constructor should instantiate"); let context: Context = None; - let result: Option> = futures::executor::block_on(tree.eval(NodeId(1), context)); + let result: Option> = tree.eval(NodeId(1), &context); let grouped = result.expect("The position should arrive as an Item"); let Graphic::VectorList(anchors) = grouped.element() else { @@ -400,7 +400,7 @@ fn color_list_wraps_through_the_colors_to_gradient_node() { let tree = BorrowTree::new(network, &typing_context).expect("The node constructor should instantiate"); let context: Context = None; - let result: Option> = futures::executor::block_on(tree.eval(NodeId(3), context)); + let result: Option> = tree.eval(NodeId(3), &context); let gradient = result.expect("The color list should arrive wrapped as a gradient"); assert_eq!(gradient.element().len(), 1, "The single color should become the gradient's one stop"); } @@ -445,7 +445,7 @@ fn as_graphic_converts_a_vector_list_element_wise() { let tree = BorrowTree::new(network, &typing_context).expect("The mapped constructor should instantiate"); let context: Context = None; - let result: Option> = futures::executor::block_on(tree.eval(NodeId(5), context)); + let result: Option> = tree.eval(NodeId(5), &context); let graphics = result.expect("The vector list should arrive as a graphic list"); assert_eq!(graphics.len(), 2, "Each vector should become its own graphic rather than collapsing into one group"); @@ -481,7 +481,7 @@ fn gradient_value_embeds_through_the_graphic_input_adapter() { let tree = BorrowTree::new(network, &typing_context).expect("The embedding constructor should instantiate"); let context: Context = None; - let result: Option> = futures::executor::block_on(tree.eval(NodeId(1), context)); + let result: Option> = tree.eval(NodeId(1), &context); let embedded = result.expect("The gradient should arrive as an Item"); assert!(embedded.attributes().iter_any().next().is_none(), "The fresh outer envelope describes the graphic, so it starts empty"); @@ -511,7 +511,7 @@ fn number_value_splats_through_the_vec2_input_adapter() { let tree = BorrowTree::new(network, &typing_context).expect("The splat constructor should instantiate"); let context: Context = None; - let result: Option> = futures::executor::block_on(tree.eval(NodeId(1), context)); + let result: Option> = tree.eval(NodeId(1), &context); assert_eq!(result.map(|item| *item.element()), Some(glam::DVec2::splat(-60.)), "The scalar should splat into both axes"); } @@ -533,7 +533,7 @@ fn number_value_formats_through_the_string_input_adapter() { let tree = BorrowTree::new(network, &typing_context).expect("The formatting constructor should instantiate"); let context: Context = None; - let result: Option> = futures::executor::block_on(tree.eval(NodeId(1), context)); + let result: Option> = tree.eval(NodeId(1), &context); assert_eq!(result.map(|item| item.element().clone()), Some("42".to_string()), "The number should format as its text representation"); } @@ -556,7 +556,7 @@ fn bool_value_embeds_through_the_number_input_adapter() { let tree = BorrowTree::new(network, &typing_context).expect("The embedding constructor should instantiate"); let context: Context = None; - let result: Option> = futures::executor::block_on(tree.eval(NodeId(1), context)); + let result: Option> = tree.eval(NodeId(1), &context); assert_eq!(result.map(|item| *item.element()), Some(expected), "{value} should embed as exactly {expected}"); } } @@ -580,7 +580,7 @@ fn bool_value_formats_through_the_string_input_adapter() { let tree = BorrowTree::new(network, &typing_context).expect("The formatting constructor should instantiate"); let context: Context = None; - let result: Option> = futures::executor::block_on(tree.eval(NodeId(1), context)); + let result: Option> = tree.eval(NodeId(1), &context); assert_eq!(result.map(|item| item.element().clone()), Some(expected.to_string()), "{value} should format as `{expected}`"); } } @@ -605,7 +605,7 @@ fn list_wire_erases_through_the_list_dyn_input_adapter() { let tree = BorrowTree::new(network, &typing_context).expect("The erasure constructor should instantiate"); let context: Context = None; - let result: Option = futures::executor::block_on(tree.eval(NodeId(1), context)); + let result: Option = tree.eval(NodeId(1), &context); let erased = result.expect("The erased list should arrive as a ListDyn"); assert_eq!(erased.len(), 3, "The erased list should keep its row count"); } @@ -627,7 +627,7 @@ fn value_wire_passes_through_the_input_adapter_as_item() { let tree = BorrowTree::new(network, &typing_context).expect("The passthrough constructor should instantiate"); let context: Context = None; - let result: Option> = futures::executor::block_on(tree.eval(NodeId(1), context)); + let result: Option> = tree.eval(NodeId(1), &context); assert_eq!(result.map(|item| *item.element()), Some(3.), "The value should arrive as an Item"); } @@ -653,7 +653,7 @@ fn modification_value_rides_the_item_wire_through_its_input_adapter() { let tree = BorrowTree::new(network, &typing_context).expect("The passthrough constructor should instantiate"); let context: Context = None; - let result: Option>> = futures::executor::block_on(tree.eval(NodeId(1), context)); + let result: Option>> = tree.eval(NodeId(1), &context); assert!(result.is_some(), "The modification should arrive as an Item"); } @@ -677,7 +677,7 @@ fn item_wire_boxes_into_the_attribute_value_connector() { let tree = BorrowTree::new(network, &typing_context).expect("The boxing constructor should instantiate"); let context: Context = None; - let result: Option> = futures::executor::block_on(tree.eval(NodeId(1), context)); + let result: Option> = tree.eval(NodeId(1), &context); let boxed = result.expect("The boxed attribute value should arrive as an Item"); assert_eq!( boxed.element().0.as_any().downcast_ref::(), @@ -691,10 +691,10 @@ fn list_wire_variant_resolves_and_executes() { let tree = compile_bounding_box_network(TaggedValue::TypeDefault(list!(Vector))); let context: Context = None; - let result: Option> = futures::executor::block_on(tree.eval(NodeId(1), context.clone())); + let result: Option> = tree.eval(NodeId(1), &context); assert!(result.is_some(), "The mapped List wire variant should downcast and execute end-to-end"); - let wrong_type: Option> = futures::executor::block_on(tree.eval(NodeId(1), context)); + let wrong_type: Option> = tree.eval(NodeId(1), &context); assert!(wrong_type.is_none(), "A List wire should not downcast as an Item"); } @@ -735,7 +735,7 @@ fn expander_flattens_under_the_frame() { let tree = BorrowTree::new(network, &typing_context).expect("The expander variant should instantiate"); let context: Context = None; - let result: Option> = futures::executor::block_on(tree.eval(output, context)); + let result: Option> = tree.eval(output, &context); let list = result.expect("An Item-wired expander should produce a List"); assert_eq!(list.len(), 2, "Splitting \"a,b\" on the comma should expand into two rows"); let substrings: Vec<_> = list.iter_element_values().map(|s| s.as_str()).collect(); @@ -780,7 +780,7 @@ fn whole_list_switches_as_one_bundle() { let tree = BorrowTree::new(network, &typing_context).expect("The bundle, wrap, and unbundle adapters should instantiate"); let context: Context = None; - let result: Option> = futures::executor::block_on(tree.eval(NodeId(4), context)); + let result: Option> = tree.eval(NodeId(4), &context); let list = result.expect("The whole stack should round-trip through the bundle switch back to a flat List"); let values: Vec = list.iter_element_values().copied().collect(); assert_eq!(values, [1., 2., 3.], "The taken branch's whole list should come through unchanged"); @@ -815,7 +815,7 @@ fn a_bundle_unbundles_into_a_list_connector() { let tree = BorrowTree::new(network, &typing_context).expect("The unbundle adapter should instantiate"); let context: Context = None; - let result: Option> = futures::executor::block_on(tree.eval(NodeId(3), context)); + let result: Option> = tree.eval(NodeId(3), &context); assert!(result.is_some(), "The unbundled stack should flow into Extend as a List"); } @@ -857,7 +857,7 @@ fn a_whole_list_of_scalars_switches_as_one_bundle() { let tree = BorrowTree::new(network, &typing_context).expect("The bundle, wrap, and unbundle adapters should instantiate"); let context: Context = None; - let result: Option> = futures::executor::block_on(tree.eval(NodeId(4), context)); + let result: Option> = tree.eval(NodeId(4), &context); let list = result.expect("The whole scalar list should round-trip through the bundle switch"); assert_eq!(list.len(), 2, "The true branch's whole list should be selected and preserved intact"); } diff --git a/node-graph/nodes/graphic/src/artboard.rs b/node-graph/nodes/graphic/src/artboard.rs index eab78e67465..9a4498a69fa 100644 --- a/node-graph/nodes/graphic/src/artboard.rs +++ b/node-graph/nodes/graphic/src/artboard.rs @@ -1,6 +1,6 @@ use core_types::list::{Item, List}; use core_types::transform::TransformMut; -use core_types::{ATTR_BACKGROUND, ATTR_CLIP, ATTR_DIMENSIONS, ATTR_LOCATION, CloneVarArgs, Color, Context, Ctx, ExtractAll, OwnedContextImpl}; +use core_types::{ATTR_BACKGROUND, ATTR_CLIP, ATTR_DIMENSIONS, ATTR_LOCATION, CloneVarArgs, Color, Context, Ctx, ExtractAll, Interrupt, OwnedContextImpl}; use glam::{DAffine2, DVec2}; use graphic_types::graphic::{Graphic, IntoGraphicList}; use graphic_types::{Artboard, Vector}; @@ -9,7 +9,7 @@ use vector_types::Gradient; /// Constructs a single-element `Artboard[]` with the given content and metadata stored as row attributes. #[node_macro::node(category(""))] -async fn create_artboard( +fn create_artboard( ctx: impl ExtractAll + CloneVarArgs + Ctx, /// Graphics to include within the artboard. #[implementations( @@ -34,7 +34,7 @@ async fn create_artboard( /// Whether to cut off the contained content that extends outside the artboard, or keep it visible. #[default(true)] clip: Item, -) -> Item { +) -> Result, Interrupt> { let (location, dimensions, clip) = (location.into_element(), dimensions.into_element(), clip.into_element()); let footprint = ctx.try_footprint().copied(); @@ -43,7 +43,7 @@ async fn create_artboard( footprint.translate(location); new_ctx = new_ctx.with_footprint(footprint); } - let content = content.eval(new_ctx.into_context()).await.into_graphic_list(); + let content = content.eval(new_ctx.into_context())?.into_graphic_list(); // Normalize so `location` is the top-left corner and `dimensions` are positive (allowing negative input // dimensions to represent dragging from the opposite corner). Compute the corner using the raw signed @@ -54,9 +54,9 @@ async fn create_artboard( let background = background.into_element(); // Name is not stored here, it's resolved live from the parent layer's display name - Item::new_from_element(Artboard::new(content)) + Ok(Item::new_from_element(Artboard::new(content)) .with_attribute(ATTR_LOCATION, normalized_location) .with_attribute(ATTR_DIMENSIONS, normalized_dimensions) .with_attribute(ATTR_BACKGROUND, background) - .with_attribute(ATTR_CLIP, clip) + .with_attribute(ATTR_CLIP, clip)) } diff --git a/node-graph/nodes/graphic/src/graphic.rs b/node-graph/nodes/graphic/src/graphic.rs index 9f5f338a0f0..17cdd13535b 100644 --- a/node-graph/nodes/graphic/src/graphic.rs +++ b/node-graph/nodes/graphic/src/graphic.rs @@ -1,7 +1,7 @@ use brush_types::Stroke; use core_types::bounds::{BoundingBox, RenderBoundingBox}; use core_types::list::{AttributeValueDyn, Item, List, ListDyn, NodeIdPath}; -use core_types::{ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_TRANSFORM, AnyHash, BlendMode, CacheHash, CloneVarArgs, Color, Context, Ctx, ExtractAll, OwnedContextImpl}; +use core_types::{ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_TRANSFORM, AnyHash, BlendMode, CacheHash, CloneVarArgs, Color, Context, Ctx, ExtractAll, Interrupt, OwnedContextImpl}; use glam::{DAffine2, DVec2}; use graphic_types::graphic::{Graphic, IntoGraphicList, is_lone_anonymous_leaf}; use graphic_types::{Artboard, Vector}; @@ -383,7 +383,7 @@ fn sort( +fn map( ctx: impl Ctx + CloneVarArgs + ExtractAll, #[implementations( List, @@ -417,22 +417,22 @@ async fn map( Context -> List, )] mapped: impl Node, Output = List>, -) -> List { +) -> Result, Interrupt> { let mut rows = List::new(); for (i, row) in content.into_iter().enumerate() { let owned_ctx = OwnedContextImpl::from(ctx.clone()); let owned_ctx = owned_ctx.with_vararg(Box::new(row)).with_index(i); - let list = mapped.eval(owned_ctx.into_context()).await; + let list = mapped.eval(owned_ctx.into_context())?; rows.extend(list); } - rows + Ok(rows) } #[node_macro::node(category("General"))] -fn mirror( +fn mirror( _: impl Ctx, #[implementations( Graphic, @@ -516,7 +516,7 @@ fn path_of_subgraph(_: impl Ctx, node_path: Item) -> Item` by the auto-inserted input adapter, so this node only /// monomorphizes over `T` instead of the cartesian product `(T, U)`. #[node_macro::node(category("Attributes: Write"))] -async fn write_attribute( +fn write_attribute( ctx: impl ExtractAll + CloneVarArgs + Ctx, /// The list to set the named attribute on (one value per item). #[implementations( @@ -541,18 +541,18 @@ async fn write_attribute( /// The attribute name (key) to write or replace. name: Item, /// The node that produces the attribute value for each item. Called once per item with the item's index in context. - value: impl Node<'n, Context<'static>, Output = Item>, -) -> List { + value: impl Node, Output = Item>, +) -> Result, Interrupt> { let name = name.into_element(); let mut content = content; for index in 0..content.len() { let row = content.clone_item(index).expect("index is within bounds"); let owned_ctx = OwnedContextImpl::from(ctx.clone()).with_vararg(Box::new(row)).with_index(index); - let v = value.eval(owned_ctx.into_context()).await.into_element(); + let v = value.eval(owned_ctx.into_context())?.into_element(); content.set_attribute_value_dyn(&name, index, v); } - content + Ok(content) } /// Reads a named `Vector` attribute from the input list, outputting each value as an element of a new `Vector[]`. @@ -813,7 +813,7 @@ fn read_attribute_raster( /// Joins two lists of the same type, extending the base list with the items from the new list. #[node_macro::node(category("General"))] -fn extend( +fn extend( _: impl Ctx, /// The list whose items will appear at the start of the extended list. #[implementations( @@ -863,7 +863,7 @@ fn extend( /// Performs an obsolete function as part of a migration from an older document format. /// Users are advised to delete this node and replace it with a new one. #[node_macro::node(category(""))] -fn legacy_layer_extend( +fn legacy_layer_extend( _: impl Ctx, #[implementations(List, List, List, List, List>, List>, List, List)] base: List, #[expose] @@ -890,7 +890,7 @@ fn legacy_layer_extend( /// Nests the input graphical content in a wrapper graphic, collecting it all into a single group. /// The inverse of this node is 'Flatten Graphic'. #[node_macro::node(category("General"))] -fn into_group + 'n>( +fn into_group>( _: impl Ctx, #[implementations( List, diff --git a/node-graph/nodes/gstd/src/render_cache.rs b/node-graph/nodes/gstd/src/render_cache.rs index cf8b93ac7d2..25991047af5 100644 --- a/node-graph/nodes/gstd/src/render_cache.rs +++ b/node-graph/nodes/gstd/src/render_cache.rs @@ -3,7 +3,7 @@ use core_types::list::Item; use core_types::math::bbox::AxisAlignedBbox; use core_types::transform::{Footprint, RenderQuality, Transform}; -use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime, Generations, OwnedContextImpl}; +use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime, Generations, Interrupt, OwnedContextImpl}; use glam::{DAffine2, DVec2, IVec2, UVec2}; use graph_craft::application_io::PlatformEditorApi; use graph_craft::document::value::{RenderOutput, RenderOutputType}; @@ -325,25 +325,25 @@ fn flood_fill(start: &TileCoord, tile_set: &HashSet, visited: &mut Ha } #[node_macro::node(category(""))] -async fn render_output_cache( +fn render_output_cache( ctx: impl Ctx + ExtractAll + CloneVarArgs + ExtractRealTime + ExtractAnimationTime + ExtractPointerPosition + Sync, #[scope(crate::platform_application_io::try_wgpu_executor::IDENTIFIER)] executor: Item>, #[scope(crate::platform_application_io::editor_api::IDENTIFIER)] editor_api: Item>, data: impl Node, Output = Item> + Send + Sync, #[data] tile_cache: TileCache, -) -> Item { +) -> Result, Interrupt> { let footprint = ctx.footprint(); let Some(render_params) = ctx.vararg(0).ok().and_then(|v| v.downcast_ref::()) else { log::warn!("render_output_cache: missing or invalid render params, falling back to direct render"); let context = OwnedContextImpl::from(ctx.clone()).with_footprint(*footprint); - return data.eval(context.into_context()).await; + return data.eval(context.into_context()); }; // Fall back to direct render for non-Vello or zero-size viewports let physical_resolution = footprint.resolution; if !matches!(render_params.render_output_type, RenderOutputTypeRequest::Vello) || physical_resolution.x == 0 || physical_resolution.y == 0 { let context = OwnedContextImpl::from(ctx.clone()).with_footprint(*footprint).with_vararg(Box::new(render_params.clone())); - return data.eval(context.into_context()).await; + return data.eval(context.into_context()); } let zoom = footprint.scale_magnitudes().x; @@ -380,7 +380,7 @@ async fn render_output_cache( if missing_region.tiles.is_empty() { continue; } - let region = render_missing_region(missing_region, |ctx| data.eval(ctx), ctx.clone(), render_params, &footprint.transform, &device_origin_offset).await; + let region = render_missing_region(missing_region, |ctx| data.eval(ctx), ctx.clone(), render_params, &footprint.transform, &device_origin_offset)?; new_regions.push(region); } @@ -391,7 +391,7 @@ async fn render_output_cache( // If no regions, fall back to direct render if all_regions.is_empty() { let context = OwnedContextImpl::from(ctx.clone()).with_footprint(*footprint).with_vararg(Box::new(render_params.clone())); - return data.eval(context.into_context()).await; + return data.eval(context.into_context()); } let executor = executor.into_element().expect("GPU executor not available"); @@ -399,23 +399,22 @@ async fn render_output_cache( let combined_metadata = composite_cached_regions(&all_regions, &output_texture, &device_origin_offset, &footprint.transform, &executor); - Item::new_from_element(RenderOutput { + Ok(Item::new_from_element(RenderOutput { data: RenderOutputType::Texture(output_texture), metadata: combined_metadata, - }) + })) } -async fn render_missing_region( +fn render_missing_region( region: &RenderRegion, render_fn: F, ctx: impl Ctx + ExtractAll + CloneVarArgs, render_params: &RenderParams, viewport_transform: &DAffine2, viewport_origin_offset: &DVec2, -) -> CachedRegion +) -> Result where - F: Fn(Context<'static>) -> Fut, - Fut: std::future::Future>, + F: Fn(Context<'static>) -> Result, Interrupt>, { let min_tile = region.tiles.iter().fold(IVec2::new(i32::MAX, i32::MAX), |acc, t| acc.min(IVec2::new(t.x, t.y))); let max_tile = region.tiles.iter().fold(IVec2::new(i32::MIN, i32::MIN), |acc, t| acc.max(IVec2::new(t.x, t.y))); @@ -433,7 +432,7 @@ where let region_params = render_params.clone(); let region_ctx = OwnedContextImpl::from(ctx).with_footprint(region_footprint).with_vararg(Box::new(region_params)).into_context(); - let mut result = render_fn(region_ctx).await.into_element(); + let mut result = render_fn(region_ctx)?.into_element(); let RenderOutputType::Texture(texture) = result.data else { unreachable!("render_missing_region: expected texture output from Vello render"); @@ -444,14 +443,14 @@ where let memory_size = (region_pixel_size.x * region_pixel_size.y) as usize * BYTES_PER_PIXEL; - CachedRegion { + Ok(CachedRegion { texture, texture_size: region_pixel_size, tiles: region.tiles.clone(), metadata: result.metadata, last_access: 0, memory_size, - } + }) } fn composite_cached_regions( diff --git a/node-graph/nodes/gstd/src/render_node.rs b/node-graph/nodes/gstd/src/render_node.rs index ac12e494eb5..035eb158632 100644 --- a/node-graph/nodes/gstd/src/render_node.rs +++ b/node-graph/nodes/gstd/src/render_node.rs @@ -1,7 +1,7 @@ use core_types::list::{Item, List}; use core_types::transform::{Footprint, Transform}; use core_types::{CloneVarArgs, ExtractAll, ExtractVarArgs}; -use core_types::{Color, Context, Ctx, ExtractFootprint, OwnedContextImpl, WasmNotSend}; +use core_types::{Color, Context, Ctx, ExtractFootprint, Interrupt, OwnedContextImpl, WasmNotSend}; use graph_craft::document::value::{RenderOutput, RenderOutputType}; use graphene_application_io::{ExportFormat, RenderConfig}; use graphic_types::raster_types::{CPU, Raster}; @@ -23,7 +23,7 @@ pub struct RenderIntermediate { } #[node_macro::node(category(""))] -async fn render_intermediate( +fn render_intermediate( ctx: impl Ctx + ExtractVarArgs + ExtractAll + CloneVarArgs, #[implementations( Context -> List, @@ -35,7 +35,7 @@ async fn render_intermediate( Context -> List, )] data: impl Node, Output = T>, -) -> Item { +) -> Result, Interrupt> { let render_params = ctx .vararg(0) .expect("Did not find var args") @@ -43,7 +43,7 @@ async fn render_intermediate( .expect("Downcasting render params yielded invalid type"); let ctx = OwnedContextImpl::from(ctx.clone()).into_context(); - let data = data.eval(ctx).await; + let data = data.eval(ctx)?; let footprint = Footprint::default(); let mut metadata = RenderMetadata::default(); @@ -72,7 +72,7 @@ async fn render_intermediate( } }; - Item::new_from_element(intermediate) + Ok(Item::new_from_element(intermediate)) } #[node_macro::node(category(""))] @@ -146,11 +146,11 @@ fn render( } #[node_macro::node(category(""))] -async fn create_context( +fn create_context( // The executor boundary supplies the render config as the sole vararg (see `wrap_network_in_scope()`) ctx: impl Ctx + ExtractAll + CloneVarArgs + Sync, data: impl Node, Output = Item>, -) -> Item { +) -> Result, Interrupt> { let render_config = ctx.vararg(0).ok().and_then(|config| config.downcast_ref::()).copied().unwrap_or_else(|| { log::error!("The boundary context is missing its render config vararg"); RenderConfig::default() @@ -185,8 +185,8 @@ async fn create_context( .with_generations(ctx.try_generations().cloned().unwrap_or_default()) .into_context(); - let mut result = data.eval(ctx).await; + let mut result = data.eval(ctx)?; result.element_mut().metadata.apply_transform(glam::DAffine2::from_scale(glam::DVec2::splat(1. / render_config.scale))); - result + Ok(result) } diff --git a/node-graph/nodes/gstd/src/render_pixel_preview.rs b/node-graph/nodes/gstd/src/render_pixel_preview.rs index 14bd50ed536..7f4bc7cc0ac 100644 --- a/node-graph/nodes/gstd/src/render_pixel_preview.rs +++ b/node-graph/nodes/gstd/src/render_pixel_preview.rs @@ -1,6 +1,6 @@ use core_types::list::Item; use core_types::transform::{Footprint, Transform}; -use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, OwnedContextImpl}; +use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, Interrupt, OwnedContextImpl}; use glam::{DAffine2, DVec2, UVec2, Vec2}; use graph_craft::document::value::{RenderOutput, RenderOutputType}; use graphic_types::raster_types::Texture; @@ -9,15 +9,15 @@ use vector_types::vector::style::RenderMode; use wgpu_executor::{WgpuExecutor, WgpuPipeline, WgpuPipelineCache}; #[node_macro::node(category(""))] -async fn render_pixel_preview( +fn render_pixel_preview( ctx: impl Ctx + ExtractAll + CloneVarArgs + Sync, #[scope(pixel_preview_pipeline::IDENTIFIER)] pipeline: Item, data: impl Node, Output = Item> + Send + Sync, -) -> Item { +) -> Result, Interrupt> { let Some(render_params) = ctx.vararg(0).ok().and_then(|v| v.downcast_ref::()).cloned() else { log::error!("invalid render params for pixel preview"); let context = OwnedContextImpl::from(ctx).into_context(); - return data.eval(context).await; + return data.eval(context); }; let physical_scale = render_params.scale; @@ -26,7 +26,7 @@ async fn render_pixel_preview( if render_params.render_mode != RenderMode::PixelPreview || !matches!(render_params.render_output_type, RenderOutputTypeRequest::Vello) || viewport_zoom <= 1. { let context = OwnedContextImpl::from(ctx).into_context(); - return data.eval(context).await; + return data.eval(context); } let physical_resolution = footprint.resolution; @@ -53,10 +53,10 @@ async fn render_pixel_preview( }; let new_ctx = OwnedContextImpl::from(ctx).with_footprint(upstream_footprint).with_vararg(Box::new(render_params)).into_context(); - let mut result = data.eval(new_ctx).await.into_element(); + let mut result = data.eval(new_ctx)?.into_element(); let RenderOutputType::Texture(ref source_texture) = result.data else { - return Item::new_from_element(result); + return Ok(Item::new_from_element(result)); }; let logical_transform = DAffine2::from_scale(DVec2::splat(1. / physical_scale)) * footprint.transform; @@ -72,7 +72,7 @@ async fn render_pixel_preview( result.metadata.apply_transform(footprint.transform * DAffine2::from_translation(upstream_min)); - Item::new_from_element(result) + Ok(Item::new_from_element(result)) } #[node_macro::node(category(""), inject_scope)] diff --git a/node-graph/nodes/math/src/lib.rs b/node-graph/nodes/math/src/lib.rs index 447bb7665c9..cc49eb29c5b 100644 --- a/node-graph/nodes/math/src/lib.rs +++ b/node-graph/nodes/math/src/lib.rs @@ -1,9 +1,9 @@ -use core_types::Context; use core_types::FallibleVec2Operations; use core_types::context::{CloneVarArgs, ExtractAll}; use core_types::list::{Bundle, Item, List}; use core_types::transform::Footprint; use core_types::{Color, Ctx, OwnedContextImpl, num_traits}; +use core_types::{Context, Interrupt}; use glam::{DAffine2, DMat2, DVec2}; use graphic_types::raster_types::{CPU, GPU, Raster}; use graphic_types::{Artboard, Graphic, Vector}; @@ -1376,7 +1376,7 @@ fn logical_not( /// Evaluates either the "If True" or "If False" input branch based on whether the input condition is true or false. #[node_macro::node(category("Math: Logic"))] -async fn switch( +fn switch( ctx: impl Ctx + CloneVarArgs + ExtractAll, condition: Item, #[expose] @@ -1439,10 +1439,10 @@ async fn switch( Context -> Item>, )] if_false: impl Node, Output = Item>, -) -> Item { +) -> Result, Interrupt> { let ctx = OwnedContextImpl::from(ctx).into_context(); - if *condition.element() { if_true.eval(ctx).await } else { if_false.eval(ctx).await } + if *condition.element() { if_true.eval(ctx) } else { if_false.eval(ctx) } } /// Constructs a `Bool` value which may be set to true or false. @@ -2304,8 +2304,8 @@ mod test { #[test] pub fn foo() { - let fnn = FnNode::new(|(a, b)| (b, a)); - assert_eq!(fnn.eval((1u32, 2u32)), (2, 1)); + let fnn = FnNode::new(|&(a, b): &(u32, u32)| (b, a)); + assert_eq!(fnn.eval(&(1u32, 2u32)).value(), Some((2, 1))); } #[test] diff --git a/node-graph/nodes/raster/src/blending_nodes.rs b/node-graph/nodes/raster/src/blending_nodes.rs index f41b17d5e73..62e759016d1 100644 --- a/node-graph/nodes/raster/src/blending_nodes.rs +++ b/node-graph/nodes/raster/src/blending_nodes.rs @@ -137,8 +137,8 @@ mod test { use raster_types::Image; use raster_types::Raster; - #[tokio::test] - async fn color_overlay_multiply() { + #[test] + fn color_overlay_multiply() { let image_color = Color::from_rgbaf32_unchecked(0.7, 0.6, 0.5, 0.4); let image = Image::new(1, 1, image_color); diff --git a/node-graph/nodes/repeat/src/repeat_nodes.rs b/node-graph/nodes/repeat/src/repeat_nodes.rs index 1637ba3c0b1..a6c7dfb445a 100644 --- a/node-graph/nodes/repeat/src/repeat_nodes.rs +++ b/node-graph/nodes/repeat/src/repeat_nodes.rs @@ -1,14 +1,14 @@ use crate::gcore::Context; use core::f64::consts::TAU; use core_types::list::{Item, List}; -use core_types::{ATTR_TRANSFORM, CloneVarArgs, Color, Ctx, ExtractAll, InjectVarArgs, OwnedContextImpl}; +use core_types::{ATTR_TRANSFORM, CloneVarArgs, Color, Ctx, ExtractAll, InjectVarArgs, Interrupt, OwnedContextImpl}; use glam::{DAffine2, DVec2}; use graphic_types::{Artboard, Graphic, Vector}; use raster_types::{CPU, GPU, Raster}; use vector_types::Gradient; #[node_macro::node(category("Repeat"))] -async fn repeat( +fn repeat( ctx: impl ExtractAll + CloneVarArgs + Ctx, #[implementations( Context -> List, @@ -25,12 +25,12 @@ async fn repeat( Context -> List, Context -> List, )] - content: impl Node<'n, Context<'static>, Output = List>, + content: impl Node, Output = List>, #[default(1)] #[hard(1..)] count: Item, reverse: Item, -) -> List { +) -> Result, Interrupt> { // Someday this node can have the option to generate infinitely instead of a fixed count (basically `std::iter::repeat`). let (count, reverse) = (count.into_element(), reverse.into_element()); @@ -42,18 +42,18 @@ async fn repeat( let index = if reverse { count - index - 1 } else { index }; let new_ctx = OwnedContextImpl::from(ctx.clone()).with_index(index); - let generated_content = content.eval(new_ctx.into_context()).await; + let generated_content = content.eval(new_ctx.into_context())?; for generated_row in generated_content.into_iter() { result_list.push(generated_row); } } - result_list + Ok(result_list) } #[node_macro::node(category("Repeat"))] -async fn repeat_array( +fn repeat_array( ctx: impl ExtractAll + CloneVarArgs + Ctx, #[implementations( Context -> List, @@ -70,7 +70,7 @@ async fn repeat_array( Context -> List, Context -> List, )] - content: impl Node<'n, Context<'static>, Output = List>, + content: impl Node, Output = List>, #[unit(" px")] #[default(100., 100.)] // TODO: When using a custom Properties panel layout in document_node_definitions.rs and this default is set, the widget weirdly doesn't show up in the Properties panel. Investigation is needed. @@ -82,7 +82,7 @@ async fn repeat_array( #[default(5)] #[hard(1..)] count: Item, -) -> List { +) -> Result, Interrupt> { let (direction, angle, count) = (direction.into_element(), angle.into_element(), count.into_element()); let angle = angle.to_radians(); // A single copy has no steps between copies, so the denominator is kept at 1 to avoid `0. / 0.` producing a NaN transform @@ -96,7 +96,7 @@ async fn repeat_array( let transform = DAffine2::from_angle(angle) * DAffine2::from_translation(translation); let new_ctx = OwnedContextImpl::from(ctx.clone()).with_index(index as usize); - let generated_content = content.eval(new_ctx.into_context()).await; + let generated_content = content.eval(new_ctx.into_context())?; for row_index in 0..generated_content.len() { let Some(mut row) = generated_content.clone_item(row_index) else { continue }; @@ -110,11 +110,11 @@ async fn repeat_array( } } - result_list + Ok(result_list) } #[node_macro::node(category("Repeat"))] -async fn repeat_radial( +fn repeat_radial( ctx: impl ExtractAll + CloneVarArgs + Ctx, #[implementations( Context -> List, @@ -131,7 +131,7 @@ async fn repeat_radial( Context -> List, Context -> List, )] - content: impl Node<'n, Context<'static>, Output = List>, + content: impl Node, Output = List>, #[unit("°")] #[range] #[soft(-180..180)] @@ -142,7 +142,7 @@ async fn repeat_radial( #[default(5)] #[hard(1..)] count: Item, -) -> List { +) -> Result, Interrupt> { let (start_angle, radius, count) = (start_angle.into_element(), radius.into_element(), count.into_element()); let mut result_list = List::new(); @@ -153,7 +153,7 @@ async fn repeat_radial( let transform = angle * translation; let new_ctx = OwnedContextImpl::from(ctx.clone()).with_index(index as usize); - let generated_content = content.eval(new_ctx.into_context()).await; + let generated_content = content.eval(new_ctx.into_context())?; for row_index in 0..generated_content.len() { let Some(mut row) = generated_content.clone_item(row_index) else { continue }; @@ -167,11 +167,11 @@ async fn repeat_radial( } } - result_list + Ok(result_list) } #[node_macro::node(category("Repeat"), name("Repeat on Points"))] -async fn repeat_on_points( +fn repeat_on_points( ctx: impl ExtractAll + CloneVarArgs + Sync + Ctx + InjectVarArgs, points: List, #[implementations( @@ -189,9 +189,9 @@ async fn repeat_on_points( Context -> List, Context -> List, )] - content: impl Node<'n, Context<'static>, Output = List>, + content: impl Node, Output = List>, reverse: Item, -) -> List { +) -> Result, Interrupt> { let reverse = reverse.into_element(); let mut result_list = List::new(); @@ -200,48 +200,46 @@ async fn repeat_on_points( let Some(points_element) = points.element(points_index) else { continue }; let transform: DAffine2 = points.attribute_cloned_or_default(ATTR_TRANSFORM, points_index); - let mut iteration = async |index, point| { + let mut iteration = |index, point| -> Result<(), Interrupt> { let transformed_point = transform.transform_point2(point); let new_ctx = OwnedContextImpl::from(ctx.clone()).with_index(index).with_position(transformed_point); - let generated_content = content.eval(new_ctx.into_context()).await; + let generated_content = content.eval(new_ctx.into_context())?; for mut generated_row in generated_content.into_iter() { generated_row.attribute_mut_or_insert_default::(ATTR_TRANSFORM).translation = transformed_point; result_list.push(generated_row); } + Ok(()) }; let range = points_element.point_domain.positions().iter().enumerate(); if reverse { for (index, &point) in range.rev() { - iteration(index, point).await; + iteration(index, point)?; } } else { for (index, &point) in range { - iteration(index, point).await; + iteration(index, point)?; } } } - result_list + Ok(result_list) } #[cfg(test)] mod test { use super::*; - use core_types::Ctx; - use core_types::Node; use core_types::list::Item; use core_types::transform::Footprint; + use core_types::{LazyInput, Node, Poll, Status}; use glam::DVec2; use graphene_core::ReadPositionNode; use graphene_core::extract_xy::{ExtractXyNode, XY}; use graphic_types::Vector; use kurbo::Shape; use kurbo::{BezPath, DEFAULT_ACCURACY, Rect}; - use std::future::Future; - use std::pin::Pin; use vector_nodes::generator_nodes::RectangleNode; use vector_types::vector::misc::BoxCorners; @@ -250,13 +248,12 @@ mod test { } #[derive(Clone)] - pub struct FutureWrapperNode(T); + pub struct ValueNode(T); - impl<'i, I: Ctx, T: 'i + Clone + Send> Node<'i, I> for FutureWrapperNode { - type Output = Pin + 'i + Send>>; - fn eval(&'i self, _input: I) -> Self::Output { - let value = self.0.clone(); - Box::pin(async move { value }) + impl Node for ValueNode { + type Output = T; + fn eval(&self, _input: &I) -> Poll { + Poll::Final(self.0.clone()) } } @@ -264,30 +261,23 @@ mod test { #[derive(Clone)] pub struct RaiseToListNode(N); - impl<'i, I: 'i, N> Node<'i, I> for RaiseToListNode - where - N: Node<'i, I, Output = Pin> + 'i + Send>>>, - { - type Output = Pin> + 'i + Send>>; - fn eval(&'i self, input: I) -> Self::Output { - let future = self.0.eval(input); - Box::pin(async move { future.await.into() }) + impl>> Node for RaiseToListNode { + type Output = List; + fn eval(&self, input: &I) -> Poll> { + self.0.eval(input).map(Into::into) } } - #[tokio::test] - async fn repeat_on_points_test() { + #[test] + fn repeat_on_points_test() { let context = OwnedContextImpl::default().into_context(); let rect = RectangleNode::new( - FutureWrapperNode(()), - ExtractXyNode::new( - ReadPositionNode::new(FutureWrapperNode(()), FutureWrapperNode(Item::new_from_element(0_i64))), - FutureWrapperNode(Item::new_from_element(XY::Y)), - ), - FutureWrapperNode(Item::new_from_element(2_f64)), - FutureWrapperNode(Item::new_from_element(BoxCorners::default())), - FutureWrapperNode(Item::new_from_element(false)), - FutureWrapperNode(Item::new_from_element(false)), + ValueNode(()), + ExtractXyNode::new(ReadPositionNode::new(ValueNode(()), ValueNode(Item::new_from_element(0_i64))), ValueNode(Item::new_from_element(XY::Y))), + ValueNode(Item::new_from_element(2_f64)), + ValueNode(Item::new_from_element(BoxCorners::default())), + ValueNode(Item::new_from_element(false)), + ValueNode(Item::new_from_element(false)), ); let positions = [DVec2::new(40., 20.), DVec2::ONE, DVec2::new(-42., 9.), DVec2::new(10., 345.)]; @@ -297,7 +287,8 @@ mod test { polyline.line_to((position.x, position.y)); } let points = vector_node_from_bezpath(polyline); - let generated = super::repeat_on_points(context, points, &RaiseToListNode(rect), Item::new_from_element(false)).await; + let status = Status::new(); + let generated = super::repeat_on_points(context, points, LazyInput::new(&RaiseToListNode(rect), &status, 1), Item::new_from_element(false)).unwrap(); assert_eq!(generated.len(), positions.len()); for (position, index) in positions.into_iter().zip(0..generated.len()) { let bounds = generated @@ -310,19 +301,20 @@ mod test { } } - #[tokio::test] - async fn repeat() { + #[test] + fn repeat() { let direction = DVec2::X * 1.5; let count = 3; let context = OwnedContextImpl::default().into_context(); + let status = Status::new(); let repeated = super::repeat_array( context, - &FutureWrapperNode(vector_node_from_bezpath(Rect::new(0., 0., 1., 1.).to_path(DEFAULT_ACCURACY))), + LazyInput::new(&ValueNode(vector_node_from_bezpath(Rect::new(0., 0., 1., 1.).to_path(DEFAULT_ACCURACY))), &status, 0), Item::new_from_element(direction), Item::new_from_element(0.), Item::new_from_element(count), ) - .await; + .unwrap(); let vector_list = List::new_from_item(vector_nodes::combine_paths(Footprint::default(), List::new_from_element(Graphic::VectorList(repeated)))); let vector = vector_list.element(0).unwrap(); assert_eq!(vector.stroke_manipulator_groups().count(), 3); @@ -331,17 +323,18 @@ mod test { } } - #[tokio::test] - async fn repeat_single_copy() { + #[test] + fn repeat_single_copy() { let context = OwnedContextImpl::default().into_context(); + let status = Status::new(); let repeated = super::repeat_array( context, - &FutureWrapperNode(vector_node_from_bezpath(Rect::new(0., 0., 1., 1.).to_path(DEFAULT_ACCURACY))), + LazyInput::new(&ValueNode(vector_node_from_bezpath(Rect::new(0., 0., 1., 1.).to_path(DEFAULT_ACCURACY))), &status, 0), Item::new_from_element(DVec2::new(12., 10.)), Item::new_from_element(45.), Item::new_from_element(1), ) - .await; + .unwrap(); let vector_list = List::new_from_item(vector_nodes::combine_paths(Footprint::default(), List::new_from_element(Graphic::VectorList(repeated)))); let vector = vector_list.element(0).unwrap(); assert_eq!(vector.stroke_manipulator_groups().count(), 1); @@ -351,19 +344,20 @@ mod test { assert!(anchor.length() < 1e-5, "Expected the single copy to be untransformed, found anchor {anchor}"); } - #[tokio::test] - async fn repeat_transform_position() { + #[test] + fn repeat_transform_position() { let direction = DVec2::new(12., 10.); let count = 8; let context = OwnedContextImpl::default().into_context(); + let status = Status::new(); let repeated = super::repeat_array( context, - &FutureWrapperNode(vector_node_from_bezpath(Rect::new(0., 0., 1., 1.).to_path(DEFAULT_ACCURACY))), + LazyInput::new(&ValueNode(vector_node_from_bezpath(Rect::new(0., 0., 1., 1.).to_path(DEFAULT_ACCURACY))), &status, 0), Item::new_from_element(direction), Item::new_from_element(0.), Item::new_from_element(count), ) - .await; + .unwrap(); let vector_list = List::new_from_item(vector_nodes::combine_paths(Footprint::default(), List::new_from_element(Graphic::VectorList(repeated)))); let vector = vector_list.element(0).unwrap(); assert_eq!(vector.stroke_manipulator_groups().count(), 8); @@ -372,17 +366,18 @@ mod test { } } - #[tokio::test] - async fn repeat_radial() { + #[test] + fn repeat_radial() { let context = OwnedContextImpl::default().into_context(); + let status = Status::new(); let repeated = super::repeat_radial( context, - &FutureWrapperNode(vector_node_from_bezpath(Rect::new(-1., -1., 1., 1.).to_path(DEFAULT_ACCURACY))), + LazyInput::new(&ValueNode(vector_node_from_bezpath(Rect::new(-1., -1., 1., 1.).to_path(DEFAULT_ACCURACY))), &status, 0), Item::new_from_element(45.), Item::new_from_element(4.), Item::new_from_element(8), ) - .await; + .unwrap(); let vector_list = List::new_from_item(vector_nodes::combine_paths(Footprint::default(), List::new_from_element(Graphic::VectorList(repeated)))); let vector = vector_list.element(0).unwrap(); assert_eq!(vector.stroke_manipulator_groups().count(), 8); diff --git a/node-graph/nodes/text/src/lib.rs b/node-graph/nodes/text/src/lib.rs index 3ecab069c7d..3870fe2bead 100644 --- a/node-graph/nodes/text/src/lib.rs +++ b/node-graph/nodes/text/src/lib.rs @@ -7,6 +7,7 @@ mod text_context; mod to_path; use convert_case::{Boundary, Converter, pattern}; +use core_types::Interrupt; use core_types::graphene_hash::CacheHash; use core_types::list::{Item, List}; use core_types::math::float_noise::round_away_float_noise; @@ -944,24 +945,24 @@ fn string_join( /// Iterates over a list of strings, evaluating the mapped operation for each one. Use the **Read String** node to access the current string inside the loop. #[node_macro::node(category("Text"))] -async fn map_string( +fn map_string( ctx: impl Ctx + CloneVarArgs + ExtractAll, strings: List, #[expose] #[implementations(Context -> Item)] mapped: impl Node, Output = Item>, -) -> List { +) -> Result, Interrupt> { let mut result = List::new(); for (i, row) in strings.into_iter().enumerate() { let owned_ctx = OwnedContextImpl::from(ctx.clone()); let owned_ctx = owned_ctx.with_vararg(Box::new(row)).with_index(i); - let mapped_string = mapped.eval(owned_ctx.into_context()).await; + let mapped_string = mapped.eval(owned_ctx.into_context())?; result.push(mapped_string); } - result + Ok(result) } /// Reads the current string from within a **Map String** node's loop. diff --git a/node-graph/nodes/transform/src/transform_nodes.rs b/node-graph/nodes/transform/src/transform_nodes.rs index 621b6b8984d..2b5c897f560 100644 --- a/node-graph/nodes/transform/src/transform_nodes.rs +++ b/node-graph/nodes/transform/src/transform_nodes.rs @@ -2,7 +2,7 @@ use core::f64; use core_types::color::Color; use core_types::list::{Item, List}; use core_types::transform::{ApplyTransform, ScaleType, Transform}; -use core_types::{ATTR_TRANSFORM, CloneVarArgs, Context, Ctx, ExtractAll, InjectFootprint, ModifyFootprint, OwnedContextImpl}; +use core_types::{ATTR_TRANSFORM, CloneVarArgs, Context, Ctx, ExtractAll, InjectFootprint, Interrupt, ModifyFootprint, OwnedContextImpl}; use glam::{DAffine2, DMat2, DVec2}; use graphic_types::raster_types::{CPU, GPU, Raster}; use graphic_types::{Artboard, Graphic, Vector}; @@ -10,7 +10,7 @@ use vector_types::Gradient; /// Applies the specified transform to the input content. #[node_macro::node(category("Math: Transform"))] -async fn transform( +fn transform( ctx: impl Ctx + CloneVarArgs + ExtractAll + ModifyFootprint, #[implementations( Context -> Item, @@ -33,7 +33,7 @@ async fn transform( #[widget(ParsedWidgetOverride::Custom = "transform_skew")] #[hard(-89.9..89.9)] skew: Item, -) -> Item { +) -> Result, Interrupt> { let (translation, rotation, scale, skew) = (*translation.element(), *rotation.element(), *scale.element(), *skew.element()); let trs = DAffine2::from_scale_angle_translation(scale, rotation.to_radians(), translation); @@ -48,24 +48,24 @@ async fn transform( ctx = ctx.with_footprint(footprint); } - let mut item = content.eval(ctx.into_context()).await; + let mut item = content.eval(ctx.into_context())?; item.left_apply_transform(&matrix); - item + Ok(item) } /// The whole-`List` counterpart of `transform`, composing the matrix onto every item of a rank-1 content wire. /// Registered under the `TransformNode` identifier by manual registry rows, since the macro's element-wise variants require an `Item`-peeling primary. #[node_macro::node(category(""), skip_impl)] -async fn transform_list( +fn transform_list( ctx: impl Ctx + CloneVarArgs + ExtractAll + ModifyFootprint, content: impl Node, Output = List>, translation: Item, rotation: Item, scale: Item, #[hard(-89.9..89.9)] skew: Item, -) -> List { +) -> Result, Interrupt> { let (translation, rotation, scale, skew) = (*translation.element(), *rotation.element(), *scale.element(), *skew.element()); let trs = DAffine2::from_scale_angle_translation(scale, rotation.to_radians(), translation); @@ -80,11 +80,11 @@ async fn transform_list( ctx = ctx.with_footprint(footprint); } - let mut list = content.eval(ctx.into_context()).await; + let mut list = content.eval(ctx.into_context())?; list.left_apply_transform(&matrix); - list + Ok(list) } /// Resets the desired components of the input transform to their default values. If all components are reset, the output will be set to the identity transform. @@ -156,7 +156,7 @@ fn replace_transform( /// Obtains the transform of the input content. #[node_macro::node(category("Math: Transform"), path(core_types::vector))] -fn extract_transform(_: impl Ctx, #[implementations(Graphic, Vector, Raster, Raster, Color, Gradient, String, Artboard)] content: Item) -> Item { +fn extract_transform(_: impl Ctx, #[implementations(Graphic, Vector, Raster, Raster, Color, Gradient, String, Artboard)] content: Item) -> Item { Item::new_from_element(content.attribute_cloned_or_default(ATTR_TRANSFORM)) } diff --git a/node-graph/nodes/vector/src/vector_modification_nodes.rs b/node-graph/nodes/vector/src/vector_modification_nodes.rs index 9a78fe7fc4b..56241d850b7 100644 --- a/node-graph/nodes/vector/src/vector_modification_nodes.rs +++ b/node-graph/nodes/vector/src/vector_modification_nodes.rs @@ -35,7 +35,7 @@ fn path_modify(_ctx: impl Ctx, vector: Item, modification: Item(_ctx: impl Ctx, #[implementations(Vector, DAffine2, DVec2)] content: Item) -> Item { +fn bake_transform(_ctx: impl Ctx, #[implementations(Vector, DAffine2, DVec2)] content: Item) -> Item { let mut content = content; if let Some(transform) = content.remove_attribute::(ATTR_TRANSFORM) { content.element_mut().bake_transform(&transform); diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index 2604469b240..d0abd3be19f 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -2,6 +2,7 @@ use core::cmp::Ordering; use core::f64::consts::{PI, TAU}; use core::hash::{Hash, Hasher}; use core_types::FallibleVec2Operations; +use core_types::Interrupt; use core_types::blending::BlendMode; use core_types::bounds::{BoundingBox, RenderBoundingBox}; use core_types::list::{ATTR_APPEARANCE, Item, ItemAttributeValues, List, ListDyn, NodeIdPath}; @@ -244,7 +245,7 @@ fn assign_colors( repeat_every: Item, ) -> T where - T: VectorListIterMut + 'n + Send, + T: VectorListIterMut + Send, { let (fill, stroke, reverse, randomize) = (fill.into_element(), stroke.into_element(), reverse.into_element(), randomize.into_element()); let (seed, repeat_every) = (seed.into_element(), repeat_every.into_element()); @@ -317,7 +318,7 @@ fn fill( _transform: Item, ) -> Item where - Item: VectorItemMut + 'n + Send, + Item: VectorItemMut + Send, { let _gradient_form = _gradient_form.into_element(); let (_has_transform, _transform) = (_has_transform.into_element(), *_transform.element()); @@ -424,7 +425,7 @@ fn stroke( dash_offset: Item, ) -> Item where - Item: VectorItemMut + 'n + Send, + Item: VectorItemMut + Send, { let mut content = content; let (weight, align, cap, join, miter_limit, dash_offset) = ( @@ -473,7 +474,7 @@ fn dash_pattern( } #[node_macro::node(category("Repeat"), name("Copy to Points"), path(core_types::vector))] -fn copy_to_points( +fn copy_to_points( _: impl Ctx, points: List, /// Artwork to be copied and placed at each point. @@ -563,7 +564,7 @@ fn copy_to_points( } #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] -fn round_corners( +fn round_corners( _: impl Ctx, #[implementations(Graphic, Vector)] source: Item, #[unit(" px")] @@ -886,7 +887,7 @@ pub mod extrude_algorithms { } #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] -fn extrude(_: impl Ctx, #[implementations(Graphic, Vector)] source: Item, direction: Item, joining_algorithm: Item) -> Item { +fn extrude(_: impl Ctx, #[implementations(Graphic, Vector)] source: Item, direction: Item, joining_algorithm: Item) -> Item { V::map_vector_items(source, |source| { let mut source = source; let (direction, joining_algorithm) = (*direction.element(), *joining_algorithm.element()); @@ -897,7 +898,7 @@ fn extrude(_: impl Ctx, #[implementations(Graphic } #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] -fn box_warp(_: impl Ctx, #[implementations(Graphic, Vector)] content: Item, #[expose] rectangle: Item) -> Item { +fn box_warp(_: impl Ctx, #[implementations(Graphic, Vector)] content: Item, #[expose] rectangle: Item) -> Item { let target_transform: DAffine2 = rectangle.attribute_cloned_or_default(ATTR_TRANSFORM); let target = rectangle.into_element(); @@ -980,7 +981,7 @@ fn bilinear_interpolate(t: DVec2, quad: &[DVec2; 4]) -> DVec2 { } #[node_macro::node(category("Vector"), path(graphene_core::vector))] -fn pack_strips( +fn pack_strips( _: impl Ctx, #[implementations( List, @@ -1103,7 +1104,7 @@ where /// Automatically constructs tangents (Bézier handles) for anchor points in a vector path. #[node_macro::node(category("Vector: Modifier"), name("Auto-Tangents"), path(core_types::vector))] -fn auto_tangents( +fn auto_tangents( _: impl Ctx, #[implementations(Graphic, Vector)] source: Item, /// The amount of spread for the auto-tangents, from 0 (sharp corner) to 1 (full spread). @@ -1255,7 +1256,7 @@ fn auto_tangents( } #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] -fn bounding_box(_: impl Ctx, #[implementations(Graphic, Vector)] content: Item) -> Item { +fn bounding_box(_: impl Ctx, #[implementations(Graphic, Vector)] content: Item) -> Item { V::map_vector_items(content, |content| { let mut content = content; let result = content @@ -1308,7 +1309,7 @@ fn points_to_vector( /// Creates a polyline from a series of vector points, replacing any existing segments that may already exist. #[node_macro::node(category("Vector"), name("Points to Polyline"), path(core_types::vector))] -fn points_to_polyline(_: impl Ctx, #[implementations(Graphic, Vector)] points: Item, #[default(true)] closed: Item) -> Item { +fn points_to_polyline(_: impl Ctx, #[implementations(Graphic, Vector)] points: Item, #[default(true)] closed: Item) -> Item { let closed = *closed.element(); V::map_vector_items(points, |points| { @@ -1338,7 +1339,7 @@ fn points_to_polyline(_: impl Ctx, #[implementati /// Evens out the distances between points by applying Lloyd's relaxation, moving every interior point toward the center of its Voronoi cell. #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] -fn relax_points( +fn relax_points( _: impl Ctx, /// A vector path or point cloud to relax. #[implementations(Graphic, Vector)] @@ -1366,7 +1367,7 @@ fn relax_points( /// /// When Connect Cells is off, every cell becomes its own closed, fillable subpath. When on, the cells share their common points and segments, forming a single connected mesh. #[node_macro::node(category("Vector"), path(core_types::vector))] -fn voronoi_cells(_: impl Ctx, #[implementations(Graphic, Vector)] source: Item, connect_cells: Item) -> Item { +fn voronoi_cells(_: impl Ctx, #[implementations(Graphic, Vector)] source: Item, connect_cells: Item) -> Item { V::map_vector_items(source, |source| { let mut source = source; let connect_cells = *connect_cells.element(); @@ -1386,7 +1387,7 @@ fn voronoi_cells(_: impl Ctx, #[implementations(G /// /// When Connect Cells is off, every triangle becomes its own closed, fillable subpath. When on, the triangles share their common points and segments, forming a single connected mesh. #[node_macro::node(category("Vector"), path(core_types::vector))] -fn triangulate(_: impl Ctx, #[implementations(Graphic, Vector)] source: Item, connect_cells: Item) -> Item { +fn triangulate(_: impl Ctx, #[implementations(Graphic, Vector)] source: Item, connect_cells: Item) -> Item { V::map_vector_items(source, |source| { let mut source = source; let connect_cells = *connect_cells.element(); @@ -1496,7 +1497,7 @@ fn mesh_weld_tolerance(polygons: &[Vec]) -> f64 { } #[node_macro::node(category("Vector: Modifier"), path(core_types::vector), properties("offset_path_properties"))] -fn offset_path( +fn offset_path( _: impl Ctx, #[implementations(Graphic, Vector)] content: Item, distance: Item, @@ -1699,14 +1700,14 @@ fn solidify_stroke_list(content: List) -> List { } #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] -fn solidify_stroke(_: impl Ctx, #[implementations(Graphic, Vector)] content: Item) -> List { +fn solidify_stroke(_: impl Ctx, #[implementations(Graphic, Vector)] content: Item) -> List { // TODO: Make this node support stroke align, which it currently ignores V::solidify_strokes(content) } #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] -fn separate_subpaths(_: impl Ctx, #[implementations(Graphic, Vector)] content: Item) -> List { +fn separate_subpaths(_: impl Ctx, #[implementations(Graphic, Vector)] content: Item) -> List { V::expand_vector_items(content, |content| { let bezpaths = content.element().stroke_bezpath_iter().collect::>(); @@ -1747,22 +1748,22 @@ fn path_is_closed( } #[node_macro::node(category("Vector"), path(graphene_core::vector))] -async fn map_points( +fn map_points( ctx: impl Ctx + CloneVarArgs + ExtractAll, #[implementations(Graphic, Vector)] content: Item, mapped: impl Node, Output = Item>, -) -> Item { +) -> Result, Interrupt> { let mut content = content; for vector in V::vector_elements_mut(&mut content) { for (index, (_, position)) in vector.point_domain.positions_mut().enumerate() { let owned_ctx = OwnedContextImpl::from(ctx.clone()).with_index(index).with_position(*position); - *position = mapped.eval(owned_ctx.into_context()).await.into_element(); + *position = mapped.eval(owned_ctx.into_context())?.into_element(); } } - content + Ok(content) } /// Combines every vector path across the input into a single compound path. @@ -1816,7 +1817,7 @@ pub fn combine_paths(_: impl Ctx, content: List) -> Item { /// Convert vector geometry into a polyline composed of evenly spaced points. #[node_macro::node(category("Vector: Modifier"), path(core_types::vector), properties("sample_polyline_properties"), memoize)] -fn sample_polyline( +fn sample_polyline( _: impl Ctx, #[implementations(Graphic, Vector)] content: Item, spacing: Item, @@ -1895,7 +1896,7 @@ fn sample_polyline( /// Simplifies vector paths by reducing the number of curve segments while preserving the overall shape within the given tolerance. #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] -fn simplify( +fn simplify( _: impl Ctx, /// The vector paths to simplify. #[implementations(Graphic, Vector)] @@ -1937,7 +1938,7 @@ fn simplify( /// Decimates vector paths into polylines by sampling any curves into line segments, then removing points that don't significantly contribute to the shape using the Ramer-Douglas-Peucker algorithm. #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] -fn decimate( +fn decimate( _: impl Ctx, /// The vector paths to decimate. #[implementations(Graphic, Vector)] @@ -2064,7 +2065,7 @@ fn decimate( /// /// If multiple subpaths make up the path, the whole number part of the progression value selects the subpath and the decimal part determines the position along it. #[node_macro::node(category("Vector: Modifier"), path(graphene_core::vector))] -fn cut_path( +fn cut_path( _: impl Ctx, /// The path to insert a cut into. #[implementations(Graphic, Vector)] @@ -2115,7 +2116,7 @@ fn cut_path( /// Cuts path segments into separate disconnected pieces where each is a distinct subpath. #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] -fn cut_segments(_: impl Ctx, #[implementations(Graphic, Vector)] content: Item) -> Item { +fn cut_segments(_: impl Ctx, #[implementations(Graphic, Vector)] content: Item) -> Item { V::map_vector_items(content, |content| { let mut content = content; // Iterate through every segment and make a copy of each of its endpoints, then reassign each segment's endpoints to its own unique point copy @@ -2257,7 +2258,7 @@ fn tangent_on_path( } #[node_macro::node(category("Vector: Modifier"), path(core_types::vector), memoize)] -fn scatter_points( +fn scatter_points( _: impl Ctx, #[implementations(Graphic, Vector)] content: Item, #[unit(" px")] @@ -2303,7 +2304,7 @@ fn scatter_points( } #[node_macro::node(name("Spline"), category("Vector: Modifier"), path(core_types::vector))] -fn spline(_: impl Ctx, #[implementations(Graphic, Vector)] content: Item) -> Item { +fn spline(_: impl Ctx, #[implementations(Graphic, Vector)] content: Item) -> Item { V::map_vector_items(content, |content| { let mut content = content; // Exit early if there are no points to generate splines from. @@ -2399,7 +2400,7 @@ fn apply_point_deltas(element: &mut Vector, deltas: &[DVec2], transform: DAffine /// Perturbs the positions of anchor points in vector geometry by random amounts and directions. #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] -fn jitter_points( +fn jitter_points( _: impl Ctx, /// The vector geometry with points to be jittered. #[implementations(Graphic, Vector)] @@ -2455,7 +2456,7 @@ fn jitter_points( /// Displaces anchor points along their normal direction (perpendicular to the path) by a set distance. /// Points with 0 or 3+ segment connections have no well-defined normal and are left in place. #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] -fn offset_points( +fn offset_points( _: impl Ctx, /// The vector geometry with points to be offset. #[implementations(Graphic, Vector)] @@ -3530,27 +3531,27 @@ fn path_length(_: impl Ctx, source: Item) -> Item { } #[node_macro::node(category("Vector: Measure"), path(core_types::vector))] -async fn area(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: impl Node, Output = Item>) -> Item { +fn area(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: impl Node, Output = Item>) -> Result, Interrupt> { let new_ctx = OwnedContextImpl::from(ctx).with_footprint(Footprint::default()).into_context(); - let vector = content.eval(new_ctx).await; + let vector = content.eval(new_ctx)?; let transform: DAffine2 = vector.attribute_cloned_or_default(ATTR_TRANSFORM); let area_scale = transform.matrix2.determinant().abs(); let area = vector.element().stroke_bezpath_iter().map(|subpath| subpath.area() * area_scale).sum::(); - Item::new_from_element(area) + Ok(Item::new_from_element(area)) } #[node_macro::node(category("Vector: Measure"), path(core_types::vector))] -async fn centroid(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: impl Node, Output = Item>, centroid_type: Item) -> Item { +fn centroid(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: impl Node, Output = Item>, centroid_type: Item) -> Result, Interrupt> { let centroid_type = centroid_type.into_element(); let new_ctx = OwnedContextImpl::from(ctx).with_footprint(Footprint::default()).into_context(); - let vector = content.eval(new_ctx).await; + let vector = content.eval(new_ctx)?; let transform: DAffine2 = vector.attribute_cloned_or_default(ATTR_TRANSFORM); let position = element_centroid(vector.element(), transform, centroid_type); - Item::new_from_element(position) + Ok(Item::new_from_element(position)) } /// The area- or length-weighted centroid of one vector element's subpaths, averaging raw point positions when the weights all vanish. @@ -3585,22 +3586,19 @@ fn element_centroid(element: &Vector, transform: DAffine2, centroid_type: Centro #[cfg(test)] mod test { use super::*; - use core_types::Node; + use core_types::{Node, Poll}; use kurbo::{CubicBez, Point, Rect}; - use std::future::Future; - use std::pin::Pin; use vector_types::vector::algorithms::bezpath_algorithms::{TValue, trim_pathseg}; use vector_types::vector::algorithms::shapes::ellipse_bezpath; use vector_types::vector::misc::pathseg_abs_diff_eq; #[derive(Clone)] - pub struct FutureWrapperNode(T); + pub struct ValueNode(T); - impl<'i, T: 'i + Clone + Send> Node<'i, Footprint> for FutureWrapperNode { - type Output = Pin + 'i + Send>>; - fn eval(&'i self, _input: Footprint) -> Self::Output { - let value = self.0.clone(); - Box::pin(async move { value }) + impl Node for ValueNode { + type Output = T; + fn eval(&self, _input: &Footprint) -> Poll { + Poll::Final(self.0.clone()) } } @@ -3640,8 +3638,8 @@ mod test { assert!(result.element().segment_domain.ids().is_empty()); } - #[tokio::test] - async fn offset_path_does_not_duplicate_closing_anchors() { + #[test] + fn offset_path_does_not_duplicate_closing_anchors() { // Offsetting closed triangles must not leave each subpath with a redundant start/end anchor (a Kurbo offset // contour returns to approximately, not exactly, its start; that near-coincident point must close, not duplicate). let delaunay = super::triangulate((), vector_item_from_points(&SQUARE_WITH_CENTER), item(false)); @@ -3659,8 +3657,8 @@ mod test { assert!(subpaths > 0); } - #[tokio::test] - async fn delaunay_disconnected_cells_make_one_subpath_per_triangle() { + #[test] + fn delaunay_disconnected_cells_make_one_subpath_per_triangle() { let result = super::triangulate((), vector_item_from_points(&SQUARE_WITH_CENTER), item(false)); let vector = result.element(); // The square plus its center tessellates into four triangles, each its own closed subpath. @@ -3670,8 +3668,8 @@ mod test { assert!(!vector.use_face_fill()); } - #[tokio::test] - async fn delaunay_and_voronoi_cells_share_winding() { + #[test] + fn delaunay_and_voronoi_cells_share_winding() { fn signed_area(anchors: &[DVec2]) -> f64 { (0..anchors.len()).map(|i| anchors[i].perp_dot(anchors[(i + 1) % anchors.len()])).sum::() / 2. } @@ -3701,8 +3699,8 @@ mod test { } } - #[tokio::test] - async fn delaunay_shared_mesh_welds_points_and_shares_edges() { + #[test] + fn delaunay_shared_mesh_welds_points_and_shares_edges() { let result = super::triangulate((), vector_item_from_points(&SQUARE_WITH_CENTER), item(true)); let vector = result.element(); // The connected mesh reuses the five input points and shares edges, so it fills face by face. @@ -3712,8 +3710,8 @@ mod test { assert_eq!(vector.segment_domain.ids().len(), 8); } - #[tokio::test] - async fn voronoi_disconnected_cells_make_a_subpath_per_cell() { + #[test] + fn voronoi_disconnected_cells_make_a_subpath_per_cell() { let result = super::voronoi_cells((), vector_item_from_points(&SQUARE_WITH_CENTER), item(false)); let vector = result.element(); let cells = vector.stroke_manipulator_groups().filter(|(_, closed)| *closed).count(); @@ -3728,16 +3726,16 @@ mod test { } } - #[tokio::test] - async fn voronoi_shared_mesh_uses_face_fill() { + #[test] + fn voronoi_shared_mesh_uses_face_fill() { let result = super::voronoi_cells((), vector_item_from_points(&SQUARE_WITH_CENTER), item(true)); let vector = result.element(); assert!(vector.use_face_fill()); assert!(!vector.segment_domain.ids().is_empty()); } - #[tokio::test] - async fn voronoi_leaves_degenerate_input_untouched() { + #[test] + fn voronoi_leaves_degenerate_input_untouched() { // Two points cannot form a diagram, so the element passes through unchanged. let points = [DVec2::new(0., 0.), DVec2::new(1., 1.)]; let result = super::voronoi_cells((), vector_item_from_points(&points), item(false)); @@ -3746,8 +3744,8 @@ mod test { assert_eq!(vector.segment_domain.ids().len(), 0); } - #[tokio::test] - async fn relax_points_redistributes_anchors() { + #[test] + fn relax_points_redistributes_anchors() { // Four hull corners plus two off-center interior points. let points = [ DVec2::new(0., 0.), @@ -3775,8 +3773,8 @@ mod test { } } - #[tokio::test] - async fn bounding_box() { + #[test] + fn bounding_box() { let bounding_box = super::bounding_box((), Item::new_from_element(Vector::from_bezpath(Rect::new(-1., -1., 1., 1.).to_path(DEFAULT_ACCURACY)))); let bounding_box = bounding_box.element(); assert_eq!(bounding_box.stroke_manipulator_groups().count(), 1); @@ -3795,7 +3793,7 @@ mod test { let square = Vector::from_bezpath(Rect::new(-1., -1., 1., 1.).to_path(DEFAULT_ACCURACY)); let mut square = List::new_from_element(square); square.with_attribute_mut_or_default(ATTR_TRANSFORM, 0, |t: &mut DAffine2| *t *= DAffine2::from_angle(std::f64::consts::FRAC_PI_4)); - let bounding_box = BoundingBoxNodeMapped { content: FutureWrapperNode(square) }.eval(Footprint::default()).await; + let bounding_box = BoundingBoxNodeMapped { content: ValueNode(square) }.eval(&Footprint::default()).value().unwrap(); let bounding_box = bounding_box.element(0).unwrap(); assert_eq!(bounding_box.stroke_manipulator_groups().count(), 1); let manipulator_groups_anchors = bounding_box @@ -3812,8 +3810,8 @@ mod test { assert_eq!(manipulator_groups_anchors[i], expected_bounding_box[i]); } } - #[tokio::test] - async fn copy_to_points() { + #[test] + fn copy_to_points() { let points = Rect::new(-10., -10., 10., 10.).to_path(DEFAULT_ACCURACY); let element = Rect::new(-1., -1., 1., 1.).to_path(DEFAULT_ACCURACY); @@ -3845,8 +3843,8 @@ mod test { } } - #[tokio::test] - async fn sample_polyline() { + #[test] + fn sample_polyline() { let path = BezPath::from_vec(vec![PathEl::MoveTo(Point::ZERO), PathEl::CurveTo(Point::ZERO, Point::new(100., 0.), Point::new(100., 0.))]); let sample_polyline = super::sample_polyline( Footprint::default(), @@ -3864,8 +3862,8 @@ mod test { assert!(pos.distance(expected) < 1e-3, "Expected {expected} found {pos}"); } } - #[tokio::test] - async fn sample_polyline_adaptive_spacing() { + #[test] + fn sample_polyline_adaptive_spacing() { let path = BezPath::from_vec(vec![PathEl::MoveTo(Point::ZERO), PathEl::CurveTo(Point::ZERO, Point::new(100., 0.), Point::new(100., 0.))]); let sample_polyline = super::sample_polyline( Footprint::default(), @@ -3883,8 +3881,8 @@ mod test { assert!(pos.distance(expected) < 1e-3, "Expected {expected} found {pos}"); } } - #[tokio::test] - async fn poisson() { + #[test] + fn poisson() { let poisson_points = super::scatter_points( Footprint::default(), vector_item_from_bezpath(ellipse_bezpath(DVec2::splat(-50.), DVec2::splat(50.))), @@ -3901,8 +3899,8 @@ mod test { assert!(point.length() < 50. + 1., "Expected point in circle {point}") } } - #[tokio::test] - async fn path_length() { + #[test] + fn path_length() { let bezpath = Rect::new(100., 100., 201., 201.).to_path(DEFAULT_ACCURACY); let transform = DAffine2::from_scale(DVec2::new(2., 2.)); let item = create_vector_item(bezpath, transform); @@ -3912,15 +3910,15 @@ mod test { // 101 (each rectangle edge length) * 4 (rectangle perimeter) * 2 (scale) assert_eq!(length.into_element(), 101. * 4. * 2.); } - #[tokio::test] - async fn spline() { + #[test] + fn spline() { let spline = super::spline(Footprint::default(), vector_item_from_bezpath(Rect::new(0., 0., 100., 100.).to_path(DEFAULT_ACCURACY))); let spline = spline.element(); assert_eq!(spline.stroke_bezpath_iter().count(), 1); assert_eq!(spline.point_domain.positions(), &[DVec2::ZERO, DVec2::new(100., 0.), DVec2::new(100., 100.), DVec2::new(0., 100.)]); } - #[tokio::test] - async fn morph() { + #[test] + fn morph() { let mut rectangles = vector_node_from_bezpath(Rect::new(0., 0., 100., 100.).to_path(DEFAULT_ACCURACY)); let mut second_rectangle = rectangles.clone_item(0).unwrap(); *second_rectangle.attribute_mut_or_insert_default::(ATTR_TRANSFORM) *= DAffine2::from_translation((-100., -100.).into()); @@ -3945,8 +3943,8 @@ mod test { assert!((morphed.attribute_cloned_or_default::(ATTR_TRANSFORM, 0).translation - DVec2::new(-50., -50.)).length() < 1e-3); } - #[tokio::test] - async fn morph_interpolates_fill() { + #[test] + fn morph_interpolates_fill() { let rect = || { let mut v = Vector::default(); v.append_bezpath(Rect::new(0., 0., 100., 100.).to_path(DEFAULT_ACCURACY)); @@ -3985,8 +3983,8 @@ mod test { assert!(color.r() > 0. && color.b() > 0., "Fill should be a red-to-blue blend, got {color:?}"); } - #[tokio::test] - async fn morph_pairs_appearance_coverages_by_cover() { + #[test] + fn morph_pairs_appearance_coverages_by_cover() { let rect = || { let mut v = Vector::default(); v.append_bezpath(Rect::new(0., 0., 100., 100.).to_path(DEFAULT_ACCURACY)); @@ -4057,8 +4055,8 @@ mod test { ); } - #[tokio::test] - async fn bevel_rect() { + #[test] + fn bevel_rect() { let source = Rect::new(0., 0., 100., 100.).to_path(DEFAULT_ACCURACY); let beveled = super::bevel(Footprint::default(), vector_item_from_bezpath(source), Item::new_from_element(2_f64.sqrt() * 10.)); let beveled = beveled.element(); @@ -4079,8 +4077,8 @@ mod test { contains_segment(beveled.clone(), PathSeg::Line(Line::new(Point::new(10., 100.), Point::new(0., 90.)))); } - #[tokio::test] - async fn bevel_open_curve() { + #[test] + fn bevel_open_curve() { let curve = PathSeg::Cubic(CubicBez::new(Point::ZERO, Point::new(10., 0.), Point::new(10., 100.), Point::new(100., 0.))); let mut source = BezPath::new(); @@ -4103,8 +4101,8 @@ mod test { contains_segment(beveled.clone(), PathSeg::Line(Line::new(Point::new(-8.2, 0.), trimmed.start()))); } - #[tokio::test] - async fn bevel_with_transform() { + #[test] + fn bevel_with_transform() { let curve = PathSeg::Cubic(CubicBez::new(Point::ZERO, Point::new(10., 0.), Point::new(10., 100.), Point::new(100., 0.))); let mut source = BezPath::new(); @@ -4130,8 +4128,8 @@ mod test { contains_segment(beveled.clone(), PathSeg::Line(Line::new(Point::new(-8.2, 0.), trimmed.start()))); } - #[tokio::test] - async fn bevel_too_high() { + #[test] + fn bevel_too_high() { let mut source = BezPath::new(); source.move_to(Point::ZERO); source.line_to(Point::new(100., 0.)); @@ -4154,8 +4152,8 @@ mod test { contains_segment(beveled.clone(), PathSeg::Line(Line::new(Point::new(100., 50.), Point::new(50., 100.)))); } - #[tokio::test] - async fn bevel_repeated_point() { + #[test] + fn bevel_repeated_point() { let line = PathSeg::Line(Line::new(Point::ZERO, Point::new(100., 0.))); let point = PathSeg::Cubic(CubicBez::new(Point::new(100., 0.), Point::ZERO, Point::ZERO, Point::new(100., 0.))); let curve = PathSeg::Cubic(CubicBez::new(Point::new(100., 0.), Point::new(110., 0.), Point::new(110., 200.), Point::new(200., 0.))); @@ -4170,8 +4168,8 @@ mod test { } // A rank-0 vector reaches the same per-list styling path as a vector list, rather than being skipped - #[tokio::test] - async fn assign_colors_reaches_rank_0_vector_graphics() { + #[test] + fn assign_colors_reaches_rank_0_vector_graphics() { let leaf = Graphic::Vector(Box::new(vector_item_from_bezpath(Rect::new(0., 0., 10., 10.).to_path(DEFAULT_ACCURACY)))); let content = List::new_from_element(leaf); From 0cf17f8cbc2a3adcc8b7525b9f8f60d78c2a8a92 Mon Sep 17 00:00:00 2001 From: Timon Date: Mon, 12 Oct 2026 23:21:17 +0000 Subject: [PATCH 28/54] Rerender when a spawned task finishes --- editor/src/application.rs | 6 ++--- editor/src/node_graph_executor.rs | 2 ++ editor/src/node_graph_executor/runtime.rs | 33 +++++++++++++++++++---- 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/editor/src/application.rs b/editor/src/application.rs index 7eb2e23dbb7..2a46402fa32 100644 --- a/editor/src/application.rs +++ b/editor/src/application.rs @@ -24,10 +24,10 @@ impl Editor { let task_executor = TaskExecutor::new(); let mut dispatcher = Dispatcher::new(resource_storage, document_store); - dispatcher.message_handlers.future_message_handler = FutureMessageHandler::new(task_executor.handle(), wake); + dispatcher.message_handlers.future_message_handler = FutureMessageHandler::new(task_executor.handle(), wake.clone()); application_io.inject_resource_proxy(dispatcher.message_handlers.resource_storage_message_handler.resources()); crate::node_graph_executor::replace_application_io(application_io); - crate::node_graph_executor::replace_task_executor(task_executor.handle()); + crate::node_graph_executor::replace_task_executor(task_executor.handle(), wake); Self { dispatcher } } @@ -46,7 +46,7 @@ impl Editor { let mut application_io = PlatformApplicationIo::default(); application_io.inject_resource_proxy(editor.dispatcher.message_handlers.resource_storage_message_handler.resources()); runtime.replace_application_io(application_io); - runtime.replace_task_executor(task_executor.handle()); + runtime.replace_task_executor(task_executor.handle(), Arc::new(|| {})); (editor, runtime) } diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index ec8f6481c2d..475dfcf88ca 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -50,6 +50,7 @@ pub struct CompilationResponse { pub enum NodeGraphUpdate { ExecutionResponse(Box), CompilationResponse(CompilationResponse), + TaskCompleted, EyedropperPreview(Raster), } @@ -460,6 +461,7 @@ impl NodeGraphExecutor { node_graph_errors, }); } + NodeGraphUpdate::TaskCompleted => responses.add(PortfolioMessage::SubmitActiveGraphRender), NodeGraphUpdate::EyedropperPreview(raster) => { let (data, width, height) = raster.to_flat_u8(); responses.add(EyedropperToolMessage::PreviewImage { data, width, height }); diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index ae837751c22..aa725356934 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -1,6 +1,6 @@ use super::*; use crate::messages::frontend::utility_types::{ExportBounds, FileType}; -use crate::task::TaskExecutorHandle; +use crate::task::{Spawn, Task, TaskExecutorHandle}; use glam::{DAffine2, DVec2, UVec2}; use graph_craft::application_io::resource::ResourceRegistry; use graph_craft::application_io::{PlatformApplicationIo, PlatformEditorApi}; @@ -587,10 +587,10 @@ pub(crate) fn replace_application_io(application_io: PlatformApplicationIo) { node_runtime.replace_application_io(application_io); } } -pub(crate) fn replace_task_executor(handle: TaskExecutorHandle) { +pub(crate) fn replace_task_executor(handle: TaskExecutorHandle, wake: Wake) { let mut node_runtime = NODE_RUNTIME.lock(); if let Some(node_runtime) = &mut *node_runtime { - node_runtime.replace_task_executor(handle); + node_runtime.replace_task_executor(handle, wake); } } @@ -603,8 +603,13 @@ impl NodeRuntime { .into(); } - pub(crate) fn replace_task_executor(&mut self, handle: TaskExecutorHandle) { - self.async_runtime = Some(AsyncRuntime::new(Arc::new(handle))); + pub(crate) fn replace_task_executor(&mut self, handle: TaskExecutorHandle, wake: Wake) { + let spawn = NodeTaskExecutor { + handle, + sender: self.sender.clone(), + wake, + }; + self.async_runtime = Some(AsyncRuntime::new(Arc::new(spawn))); } fn async_runtime(&self) -> &AsyncRuntime { @@ -612,6 +617,24 @@ impl NodeRuntime { } } +struct NodeTaskExecutor { + handle: TaskExecutorHandle, + sender: Sender, + wake: Wake, +} + +impl Spawn for NodeTaskExecutor { + fn spawn(&self, task: Task) { + let sender = self.sender.clone(); + let wake = self.wake.clone(); + self.handle.spawn(Box::pin(async move { + task.await; + let _ = sender.send(NodeGraphUpdate::TaskCompleted); + wake(); + })); + } +} + /// Which node is inspected and which monitor node is used (if any) for the current execution #[derive(Debug, Clone)] struct InspectState { From 325061ed6a5791316a3740bf0a41479edd21444b Mon Sep 17 00:00:00 2001 From: Timon Date: Mon, 12 Oct 2026 23:42:44 +0000 Subject: [PATCH 29/54] Rerender in CLI until all tasks are done --- node-graph/graphene-cli/src/export.rs | 29 +++++++++++++++++---------- node-graph/graphene-cli/src/main.rs | 27 ++++++++++++++++++------- 2 files changed, 38 insertions(+), 18 deletions(-) diff --git a/node-graph/graphene-cli/src/export.rs b/node-graph/graphene-cli/src/export.rs index 23458e89347..255d2839d3a 100644 --- a/node-graph/graphene-cli/src/export.rs +++ b/node-graph/graphene-cli/src/export.rs @@ -1,10 +1,11 @@ +use crate::TokioSpawner; use graph_craft::document::value::{RenderOutputType, TaggedValue, UVec2}; use graph_craft::graphene_compiler::Executor; -use graphene_std::Generations; use graphene_std::application_io::{ExportFormat, RenderConfig, TimingInformation}; use graphene_std::core_types::ops::ConvertAsync; use graphene_std::core_types::transform::Footprint; use graphene_std::raster_types::{CPU, GPU, Raster}; +use graphene_std::runtime::AsyncRuntime; use interpreted_executor::dynamic_executor::DynamicExecutor; use std::error::Error; use std::io::Cursor; @@ -38,7 +39,8 @@ pub async fn export_document( scale: f64, (width, height): (Option, Option), transparent: bool, - generations: Generations, + runtime: &AsyncRuntime, + spawner: &TokioSpawner, ) -> Result<(), Box> { // Determine export format based on file type let export_format = match file_type { @@ -60,10 +62,7 @@ pub async fn export_document( } // Execute the graph - let result = executor - .execute(render_config.into_context_with_generations(generations))? - .value() - .ok_or("The node graph produced no frame")?; + let result = execute_until_settled(executor, runtime, spawner, render_config).await?; // Handle the result based on output type match result { @@ -155,6 +154,7 @@ impl AnimationParams { } /// Export an animated GIF by rendering multiple frames at different animation times +#[allow(clippy::too_many_arguments)] pub async fn export_gif( executor: &DynamicExecutor, wgpu_executor: &wgpu_executor::WgpuExecutor, @@ -162,7 +162,8 @@ pub async fn export_gif( scale: f64, (width, height): (Option, Option), animation: AnimationParams, - generations: Generations, + runtime: &AsyncRuntime, + spawner: &TokioSpawner, ) -> Result<(), Box> { use image::codecs::gif::{GifEncoder, Repeat}; use image::{Frame, RgbaImage}; @@ -202,10 +203,7 @@ pub async fn export_gif( } // Execute the graph for this frame - let result = executor - .execute(render_config.into_context_with_generations(generations.clone()))? - .value() - .ok_or("The node graph produced no frame")?; + let result = execute_until_settled(executor, runtime, spawner, render_config).await?; // Extract RGBA data from result let (data, img_width, img_height) = match result { @@ -238,3 +236,12 @@ pub async fn export_gif( log::info!("Exported GIF to: {}", output_path.display()); Ok(()) } + +async fn execute_until_settled(executor: &DynamicExecutor, runtime: &AsyncRuntime, spawner: &TokioSpawner, render_config: RenderConfig) -> Result> { + let execute = || executor.execute(render_config.into_context_with_generations(runtime.snapshot())); + let mut frame = execute()?; + while spawner.join().await { + frame = execute()?; + } + frame.value().ok_or_else(|| "The node graph produced no frame".into()) +} diff --git a/node-graph/graphene-cli/src/main.rs b/node-graph/graphene-cli/src/main.rs index 2fdbcdc772f..c4ad29a844c 100644 --- a/node-graph/graphene-cli/src/main.rs +++ b/node-graph/graphene-cli/src/main.rs @@ -17,7 +17,7 @@ use interpreted_executor::dynamic_executor::DynamicExecutor; use interpreted_executor::util::wrap_network_in_scope; use std::error::Error; use std::path::PathBuf; -use std::sync::Arc; +use std::sync::{Arc, Mutex}; #[derive(Debug, Parser)] #[clap(name = "graphene-cli", version)] @@ -177,7 +177,8 @@ async fn main() -> Result<(), Box> { application_io: Some(application_io_for_api), editor_preferences: Box::new(preferences), }); - let runtime = AsyncRuntime::new(Arc::new(TokioSpawner)); + let spawner = Arc::new(TokioSpawner::default()); + let runtime = AsyncRuntime::new(spawner.clone()); let proto_graph = compile_graph(node_network, editor_api, gdd.as_ref(), runtime.handle())?; match app.command { @@ -211,7 +212,6 @@ async fn main() -> Result<(), Box> { // Create executor let executor = create_executor(proto_graph)?; runtime.retain_sources(executor.sources()); - let generations = runtime.snapshot(); if fps <= 0. { return Err("Fps number must be positive".into()); @@ -220,9 +220,9 @@ async fn main() -> Result<(), Box> { // Perform export based on file type if file_type == export::FileType::Gif { let animation = export::AnimationParams::new(fps, frames, duration); - export::export_gif(&executor, wgpu_executor_ref, output, scale, (width, height), animation, generations).await?; + export::export_gif(&executor, wgpu_executor_ref, output, scale, (width, height), animation, &runtime, &spawner).await?; } else { - export::export_document(&executor, wgpu_executor_ref, output, file_type, scale, (width, height), transparent, generations).await?; + export::export_document(&executor, wgpu_executor_ref, output, file_type, scale, (width, height), transparent, &runtime, &spawner).await?; } } _ => unreachable!("All other commands should be handled before this match statement is run"), @@ -283,9 +283,22 @@ fn create_executor(proto_network: ProtoNetwork) -> Result>>); + impl Spawn for TokioSpawner { fn spawn(&self, task: Task) { - tokio::spawn(task); + self.0.lock().unwrap().push(tokio::spawn(task)); + } +} + +impl TokioSpawner { + pub(crate) async fn join(&self) -> bool { + let tasks = std::mem::take(&mut *self.0.lock().unwrap()); + let pending = !tasks.is_empty(); + for task in tasks { + let _ = task.await; + } + pending } } From 3bc1a2891da9ef75036be4a436bbf76f855071ac Mon Sep 17 00:00:00 2001 From: Timon Date: Tue, 13 Oct 2026 09:16:24 +0000 Subject: [PATCH 30/54] Abort pending tasks when node is dropped --- Cargo.lock | 1 + node-graph/libraries/core-types/Cargo.toml | 1 + .../libraries/core-types/src/runtime.rs | 104 ++++++++++++++---- node-graph/node-macro/src/codegen.rs | 10 +- 4 files changed, 88 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f6dcec7e90a..480e390d3cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -903,6 +903,7 @@ dependencies = [ "color", "ctor", "dyn-any", + "futures", "glam 0.33.10", "graphene-hash", "image", diff --git a/node-graph/libraries/core-types/Cargo.toml b/node-graph/libraries/core-types/Cargo.toml index c6856f2c1ed..865e7f1b827 100644 --- a/node-graph/libraries/core-types/Cargo.toml +++ b/node-graph/libraries/core-types/Cargo.toml @@ -50,6 +50,7 @@ lyon_geom = { workspace = true } log = { workspace = true } base64 = { workspace = true } polycool = { workspace = true } +futures = { workspace = true } # Optional workspace dependencies serde = { workspace = true, optional = true } diff --git a/node-graph/libraries/core-types/src/runtime.rs b/node-graph/libraries/core-types/src/runtime.rs index fa4097b4b2d..67bd16ba2bc 100644 --- a/node-graph/libraries/core-types/src/runtime.rs +++ b/node-graph/libraries/core-types/src/runtime.rs @@ -1,6 +1,7 @@ use crate::poll::Poll; use crate::uuid::NodeId; use dyn_any::WasmNotSend; +use futures::future::{AbortHandle, Abortable}; use std::collections::HashMap; use std::future::Future; use std::hash::{Hash, Hasher}; @@ -112,59 +113,77 @@ impl RuntimeInner { } } -pub struct MemoTable(Arc>>>>); +pub struct Slots(Arc>>>); -impl Default for MemoTable { +enum Slot { + Ready(Poll), + Pending(AbortHandle), +} + +impl Drop for Slot { + fn drop(&mut self) { + if let Slot::Pending(task) = self { + task.abort(); + } + } +} + +impl Default for Slots { fn default() -> Self { Self(Arc::default()) } } -impl Clone for MemoTable { +impl Clone for Slots { fn clone(&self) -> Self { Self(Arc::clone(&self.0)) } } -impl std::fmt::Debug for MemoTable { +impl std::fmt::Debug for Slots { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("MemoTable").finish_non_exhaustive() + f.debug_struct("Slots").finish_non_exhaustive() } } -impl MemoTable { +impl Slots { pub fn resolve(&self, key: u64, runtime: &dyn Runtime, source: SourceId, placeholder: Option, future: impl FnOnce() -> F) -> Poll where F: Future> + WasmNotSend + 'static, { - { - let mut table = self.0.lock().unwrap_or_else(PoisonError::into_inner); - match table.get(&key) { - Some(Some(landed)) => return landed.clone(), - Some(None) => return placeholder.map_or(Poll::Pending, Poll::Partial), - None => table.insert(key, None), - }; + match self.0.lock().unwrap_or_else(PoisonError::into_inner).get(&key) { + Some(Slot::Ready(landed)) => return landed.clone(), + Some(Slot::Pending(_)) => return placeholder.map_or(Poll::Pending, Poll::Partial), + None => {} } // Fast path for futures that are ready immediately // TODO(Timon): check if this would ever cause a problem; running a future on the graph executor thread might also be a bit wierd let mut future = Box::pin(future()); if let std::task::Poll::Ready(state) = future.as_mut().poll(&mut Context::from_waker(Waker::noop())) { - self.0.lock().unwrap_or_else(PoisonError::into_inner).insert(key, Some(state.clone())); + self.0.lock().unwrap_or_else(PoisonError::into_inner).insert(key, Slot::Ready(state.clone())); return state; } - let slots = Arc::clone(&self.0); + let (handle, registration) = AbortHandle::new_pair(); + let slots = Arc::downgrade(&self.0); runtime.spawn( source, Box::pin(async move { - let state = future.await; - slots.lock().unwrap_or_else(PoisonError::into_inner).insert(key, Some(state)); + if let Ok(state) = Abortable::new(future, registration).await + && let Some(slots) = slots.upgrade() + { + slots.lock().unwrap_or_else(PoisonError::into_inner).insert(key, Slot::Ready(state)); + } }), ); - match self.0.lock().unwrap_or_else(PoisonError::into_inner).get(&key) { - Some(Some(resolved)) => resolved.clone(), - _ => placeholder.map_or(Poll::Pending, Poll::Partial), + let mut slots = self.0.lock().unwrap_or_else(PoisonError::into_inner); + match slots.get(&key) { + Some(Slot::Ready(landed)) => landed.clone(), + _ => { + slots.insert(key, Slot::Pending(handle)); + placeholder.map_or(Poll::Pending, Poll::Partial) + } } } } @@ -172,6 +191,7 @@ impl MemoTable { #[cfg(test)] mod tests { use super::*; + use std::sync::atomic::{AtomicBool, Ordering}; fn poll_once(task: &mut Task) -> bool { task.as_mut().poll(&mut Context::from_waker(Waker::noop())).is_ready() @@ -240,7 +260,7 @@ mod tests { let runtime = AsyncRuntime::new(spawner.clone()); runtime.retain_sources(&[SOURCE]); - let slots = MemoTable::default(); + let slots = Slots::default(); assert_eq!(slots.resolve(1, &runtime, SOURCE, None, || async { Poll::Final(1u32) }), Poll::Final(1)); assert_eq!(spawner.drain(), 0); assert_eq!(&*runtime.snapshot(), &[(SOURCE, 0)]); @@ -256,7 +276,7 @@ mod tests { let spawner = Arc::new(CollectSpawner::default()); let runtime = AsyncRuntime::new(spawner.clone()); runtime.retain_sources(&[SOURCE]); - let slots = MemoTable::default(); + let slots = Slots::default(); let fetch = || async { YieldOnce(false).await; Poll::Final(42u32) @@ -273,7 +293,7 @@ mod tests { fn a_blocking_spawner_lands_the_slot_in_the_same_resolve() { let runtime = AsyncRuntime::new(Arc::new(BlockingSpawner)); runtime.retain_sources(&[SOURCE]); - let slots = MemoTable::default(); + let slots = Slots::default(); let fetch = || async { YieldOnce(false).await; Poll::Final(42u32) @@ -282,4 +302,42 @@ mod tests { assert_eq!(slots.resolve(1, &runtime, SOURCE, Some(0), fetch), Poll::Final(42)); assert_eq!(&*runtime.snapshot(), &[(SOURCE, 1)]); } + + #[test] + fn a_clone_keeps_the_task_running() { + let spawner = Arc::new(CollectSpawner::default()); + let runtime = AsyncRuntime::new(spawner.clone()); + let slots = Slots::default(); + let fetch = || async { + YieldOnce(false).await; + Poll::Final(42u32) + }; + + assert_eq!(slots.resolve(1, &runtime, SOURCE, None, fetch), Poll::Pending); + let clone = slots.clone(); + drop(slots); + assert_eq!(spawner.drain(), 1); + assert_eq!(clone.resolve(1, &runtime, SOURCE, None, fetch), Poll::Final(42)); + } + + #[test] + fn dropping_the_last_clone_aborts_the_task() { + let spawner = Arc::new(CollectSpawner::default()); + let runtime = AsyncRuntime::new(spawner.clone()); + let slots = Slots::default(); + let finished = Arc::new(AtomicBool::new(false)); + let fetch = || { + let finished = Arc::clone(&finished); + async move { + YieldOnce(false).await; + finished.store(true, Ordering::Relaxed); + Poll::Final(42u32) + } + }; + + assert_eq!(slots.resolve(1, &runtime, SOURCE, None, fetch), Poll::Pending); + drop(slots); + assert_eq!(spawner.drain(), 1); + assert!(!finished.load(Ordering::Relaxed)); + } } diff --git a/node-graph/node-macro/src/codegen.rs b/node-graph/node-macro/src/codegen.rs index 6e61d88e188..99c7061808e 100644 --- a/node-graph/node-macro/src/codegen.rs +++ b/node-graph/node-macro/src/codegen.rs @@ -145,8 +145,8 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn if tier == NodeOutputTier::Poll && (mapped_variant || list_content_variant) { return Err(Error::new_spanned(fn_name, "An element-wise node cannot return `Poll`")); } - let slot_field = parsed.is_async.then(|| quote! { pub(super) slot: #core_types::runtime::MemoTable<#output> }); - let struct_fields: Vec<_> = data_field_defs.chain(regular_field_defs).chain(slot_field).collect(); + let slots_field = parsed.is_async.then(|| quote! { pub(super) slots: #core_types::runtime::Slots<#output> }); + let struct_fields: Vec<_> = data_field_defs.chain(regular_field_defs).chain(slots_field).collect(); // Data fields get passed as references to the underlying function let data_field_idents: Vec<_> = data_fields.iter().map(|f| &f.pat_ident).collect(); @@ -512,8 +512,8 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let regular_inits = regular_field_names.iter().map(|name| { quote! { #name } }); - let slot_init = parsed.is_async.then(|| quote! { slot: Default::default() }); - let all_field_inits: Vec<_> = data_inits.chain(regular_inits).chain(slot_init).collect(); + let slots_init = parsed.is_async.then(|| quote! { slots: Default::default() }); + let all_field_inits: Vec<_> = data_inits.chain(regular_inits).chain(slots_init).collect(); let async_keyword = is_async.then(|| quote!(async)); @@ -624,7 +624,7 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let __runtime = _runtime.element().clone(); let __context = __input.clone(); #(#data_clones)* - let __state = self.slot.resolve(__key, &__runtime, __source, __placeholder, move || async move { + let __state = self.slots.resolve(__key, &__runtime, __source, __placeholder, move || async move { #core_types::Poll::Final(self::#fn_name(__context #(, &#data_field_names)* #(, #regular_field_names)*).await) }); __cell.merge(__state) From 35b7ead41b89277a0be2e02b8337d3096006bcbf Mon Sep 17 00:00:00 2001 From: Timon Date: Tue, 13 Oct 2026 11:12:06 +0000 Subject: [PATCH 31/54] Trace errors through Poll node inputs --- node-graph/libraries/core-types/src/lib.rs | 2 +- node-graph/libraries/core-types/src/poll.rs | 26 +++++++++++++++++++++ node-graph/node-macro/src/codegen.rs | 2 +- node-graph/nodes/gcore/src/memo.rs | 21 +++++++++++++++++ 4 files changed, 49 insertions(+), 2 deletions(-) diff --git a/node-graph/libraries/core-types/src/lib.rs b/node-graph/libraries/core-types/src/lib.rs index f7bec4eec96..00c016d900b 100644 --- a/node-graph/libraries/core-types/src/lib.rs +++ b/node-graph/libraries/core-types/src/lib.rs @@ -41,7 +41,7 @@ pub use no_std_types::choice_type; pub use no_std_types::color; pub use no_std_types::shaders; pub use num_traits; -pub use poll::{Finality, Interrupt, LazyInput, NodeError, Poll, Status}; +pub use poll::{Finality, Interrupt, LazyInput, NodeError, Poll, PollInput, Status}; pub use runtime::{Generations, RuntimeHandle, SourceId}; use std::any::TypeId; #[cfg(feature = "wasm")] diff --git a/node-graph/libraries/core-types/src/poll.rs b/node-graph/libraries/core-types/src/poll.rs index 55445831038..123239ade36 100644 --- a/node-graph/libraries/core-types/src/poll.rs +++ b/node-graph/libraries/core-types/src/poll.rs @@ -243,6 +243,32 @@ impl<'a, N> LazyInput<'a, N> { } } +pub struct PollInput<'a, N> { + node: &'a N, + index: usize, +} + +impl<'a, N> PollInput<'a, N> { + pub fn new(node: &'a N, index: usize) -> Self { + Self { node, index } + } +} + +impl> Node for PollInput<'_, N> { + type Output = N::Output; + + #[inline(always)] + fn eval(&self, input: &I) -> Poll { + let mut state = self.node.eval(input); + match &mut state { + Poll::Fallback(boxed) => boxed.1.trace.push(self.index), + Poll::Error(error) => error.trace.push(self.index), + Poll::Pending | Poll::Final(_) | Poll::Partial(_) => {} + } + state + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/node-graph/node-macro/src/codegen.rs b/node-graph/node-macro/src/codegen.rs index 99c7061808e..1d975ec78ab 100644 --- a/node-graph/node-macro/src/codegen.rs +++ b/node-graph/node-macro/src/codegen.rs @@ -335,7 +335,7 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let name = &field.pat_ident.ident; match &field.ty { ParsedFieldType::Node { .. } => match tier { - NodeOutputTier::Poll => quote! { let #name = &self.#name; }, + NodeOutputTier::Poll => quote! { let #name = &#core_types::PollInput::new(&self.#name, #index); }, _ => quote! { let #name = #core_types::LazyInput::new(&self.#name, __cell, #index); }, }, _ => quote! { let #name = __cell.record(#index, self.#name.eval(__input)); }, diff --git a/node-graph/nodes/gcore/src/memo.rs b/node-graph/nodes/gcore/src/memo.rs index 4ae8c9cd588..edf6f9cee1f 100644 --- a/node-graph/nodes/gcore/src/memo.rs +++ b/node-graph/nodes/gcore/src/memo.rs @@ -54,3 +54,24 @@ fn serialize_monitor) } + +#[cfg(test)] +mod tests { + use super::*; + use core_types::Node; + + struct Fail; + + impl Node<()> for Fail { + type Output = u32; + fn eval(&self, _: &()) -> Poll { + Poll::error("error") + } + } + + #[test] + fn an_error_records_the_index_of_the_memoized_input() { + let Poll::Error(error) = MemoizeNode::new(Fail).eval(&()) else { unreachable!() }; + assert_eq!(error.trace(), [0]); + } +} From ef3c84c8c7935dee3591790bdfcb30a265f9424b Mon Sep 17 00:00:00 2001 From: Timon Date: Wed, 14 Oct 2026 17:12:15 +0000 Subject: [PATCH 32/54] Let node errors carry upstream errors --- node-graph/libraries/core-types/src/poll.rs | 29 +++++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/node-graph/libraries/core-types/src/poll.rs b/node-graph/libraries/core-types/src/poll.rs index 123239ade36..08b3cc0f908 100644 --- a/node-graph/libraries/core-types/src/poll.rs +++ b/node-graph/libraries/core-types/src/poll.rs @@ -63,6 +63,7 @@ pub enum Interrupt { pub struct NodeError { error: Arc, trace: Vec, + upstream: Vec, } impl NodeError { @@ -70,17 +71,35 @@ impl NodeError { Self { error: Arc::from(error.into()), trace: Vec::new(), + upstream: Vec::new(), } } + pub fn with_upstream(mut self, upstream: Vec) -> Self { + self.upstream = upstream; + self + } + + pub fn track(&mut self, input: usize) { + self.trace.push(input); + } + pub fn trace(&self) -> &[usize] { &self.trace } + + pub fn upstream(&self) -> &[NodeError] { + &self.upstream + } + + pub fn error(&self) -> &Arc { + &self.error + } } impl PartialEq for NodeError { fn eq(&self, other: &Self) -> bool { - Arc::ptr_eq(&self.error, &other.error) && self.trace == other.trace + Arc::ptr_eq(&self.error, &other.error) && self.trace == other.trace && self.upstream == other.upstream } } @@ -143,13 +162,13 @@ impl Status { } Poll::Fallback(boxed) => { let (value, mut error) = *boxed; - error.trace.push(index); + error.track(index); self.fail(error); Ok(value) } Poll::Pending => Err(self.interrupted(Interrupt::Pending)), Poll::Error(mut error) => { - error.trace.push(index); + error.track(index); Err(self.interrupted(Interrupt::Error(error))) } } @@ -261,8 +280,8 @@ impl> Node for PollInput<'_, N> { fn eval(&self, input: &I) -> Poll { let mut state = self.node.eval(input); match &mut state { - Poll::Fallback(boxed) => boxed.1.trace.push(self.index), - Poll::Error(error) => error.trace.push(self.index), + Poll::Fallback(boxed) => boxed.1.track(self.index), + Poll::Error(error) => error.track(self.index), Poll::Pending | Poll::Final(_) | Poll::Partial(_) => {} } state From 52da8c7284f26d3053ec776015decee8c29be375 Mon Sep 17 00:00:00 2001 From: Timon Date: Wed, 14 Oct 2026 17:12:15 +0000 Subject: [PATCH 33/54] Resolve error traces to the origin node --- .../src/dynamic_executor.rs | 45 +++++++++++++------ .../src/dynamic_executor/test.rs | 37 +++++++++++++++ 2 files changed, 68 insertions(+), 14 deletions(-) diff --git a/node-graph/interpreted-executor/src/dynamic_executor.rs b/node-graph/interpreted-executor/src/dynamic_executor.rs index 32d4e80f235..6a3e407e3b8 100644 --- a/node-graph/interpreted-executor/src/dynamic_executor.rs +++ b/node-graph/interpreted-executor/src/dynamic_executor.rs @@ -1,5 +1,5 @@ use crate::node_registry; -use core_types::{Poll, SourceId, WasmNotSend}; +use core_types::{NodeError, Poll, SourceId, WasmNotSend}; use graph_craft::Type; use graph_craft::document::NodeId; use graph_craft::document::value::{TaggedValue, UpcastNode}; @@ -138,6 +138,23 @@ impl DynamicExecutor { // TODO: https://github.com/GraphiteEditor/Graphite/issues/1767 // TODO: Non exposed inputs are not added to the inputs_source_map, so they are not included in the resolved_document_node_types. The type is still available in the typing_context. This only affects the UI-only "Import" node. } + + // TODO(Dennis and Timon): Remove this once rank promotion is fixed. + // Only really needed due to the runtime not knowing about rank adapters. + // Once we reworked that we should be able to move this resolution into the runtime. + // Also remember to remove `BorrowTree::nodes.3` at that point + pub fn find_origin(&self, mut node: NodeId, error: &NodeError) -> Option { + let mut trace = error.trace().iter().rev(); + while let Some(&index) = trace.next() { + // Hack to undo rank promotion while traversing the graph + if self.typing_context.promotions(node).is_some_and(|promotions| promotions.iter().any(|(argument, _)| *argument == index)) { + trace.next(); + } + + node = *self.tree.nodes.get(&node).map(|(_, _, inputs)| &**inputs)?.get(index)?; + } + Some(node) + } } impl Executor> for &DynamicExecutor @@ -186,8 +203,8 @@ impl std::fmt::Display for IntrospectError { /// /// # Fields /// -/// * `nodes`: A [`HashMap`] of [`NodeId`]s to tuples of [`SharedNodeContainer`] and [`Path`]. -/// This stores the actual node instances and their associated paths. +/// * `nodes`: A [`HashMap`] of [`NodeId`]s to tuples of [`SharedNodeContainer`], [`Path`], and input [`NodeId`]s. +/// This stores the actual node instances, their associated paths, and the nodes connected to their inputs. /// /// * `source_map`: A [`HashMap`] from [`Path`] to tuples of [`NodeId`] and [`NodeTypes`]. /// This maps document paths to node IDs and their associated type information. @@ -195,8 +212,8 @@ impl std::fmt::Display for IntrospectError { /// A store of the dynamically typed nodes and also the source map. #[derive(Default, Clone)] pub struct BorrowTree { - /// A hashmap of node IDs and dynamically typed nodes. - nodes: HashMap, + /// A hashmap of node IDs and dynamically typed nodes, with their paths and input node IDs. + nodes: HashMap)>, /// A hashmap from the document path to the proto node ID. source_map: HashMap, } @@ -231,19 +248,19 @@ impl BorrowTree { nodes.iter().map(|node| self.nodes.get(node).unwrap().0.clone()).collect() } - fn store_node(&mut self, node: SharedNodeContainer, id: NodeId, path: Path) { - self.nodes.insert(id, (node, path)); + fn store_node(&mut self, node: SharedNodeContainer, id: NodeId, path: Path, inputs: Box<[NodeId]>) { + self.nodes.insert(id, (node, path, inputs)); } /// Calls the `Node::serialize` for that specific node, returning for example the cached value for a monitor node. The node path must match the document node path. pub fn introspect(&self, node_path: &[NodeId]) -> Result, IntrospectError> { let (id, _) = self.source_map.get(node_path).ok_or_else(|| IntrospectError::PathNotFound(node_path.to_vec()))?; - let (node, _path) = self.nodes.get(id).ok_or(IntrospectError::ProtoNodeNotFound(*id))?; + let (node, _path, _) = self.nodes.get(id).ok_or(IntrospectError::ProtoNodeNotFound(*id))?; node.serialize().ok_or(IntrospectError::NoData) } pub fn get(&self, id: NodeId) -> Option { - self.nodes.get(&id).map(|(node, _)| node.clone()) + self.nodes.get(&id).map(|(node, _, _)| node.clone()) } /// Evaluate the output node of the [`BorrowTree`]. @@ -252,7 +269,7 @@ impl BorrowTree { I: 'static + WasmNotSend, O: 'static, { - let (node, _path) = self.nodes.get(&id)?; + let (node, _path, _) = self.nodes.get(&id)?; node.eval(input).value().and_then(|output| output.downcast::().ok().map(|o| *o)) } /// Evaluate the output node of the [`BorrowTree`] and cast it to a tagged value. @@ -261,7 +278,7 @@ impl BorrowTree { where I: 'static + WasmNotSend, { - let (node, _path) = self.nodes.get(&id).ok_or("Output node not found in executor")?; + let (node, _path, _) = self.nodes.get(&id).ok_or("Output node not found in executor")?; Ok(match node.eval(input) { Poll::Pending => Poll::Pending, Poll::Final(output) => Poll::Final(TaggedValue::try_from_any(output)?), @@ -330,7 +347,7 @@ impl BorrowTree { /// - If the node is the primary node for its path in the `source_map`, it's also removed from there. /// - Returns `None` if the node is not found in the `nodes` HashMap. pub fn free_node(&mut self, id: NodeId) -> Option { - let (_, path) = self.nodes.remove(&id)?; + let (_, path, _) = self.nodes.remove(&id)?; if self.source_map.get(&path)?.0 == id { self.source_map.remove(&path); return Some(path); @@ -406,7 +423,7 @@ impl BorrowTree { ConstructionArgs::Value(value) => { let upcasted = UpcastNode::new(value.to_owned()); let node = NodeContainer::new(Box::new(upcasted) as TypeErasedBox<'_>); - self.store_node(node, id, path.into()); + self.store_node(node, id, path.into(), Box::default()); } ConstructionArgs::Inline(_) => unimplemented!("Inline nodes are not supported yet"), ConstructionArgs::Nodes(ids) => { @@ -427,7 +444,7 @@ impl BorrowTree { let constructor = typing_context.constructor(id).ok_or_else(|| vec![GraphError::new(&proto_node, GraphErrorType::NoConstructor)])?; let node = constructor(construction_nodes); let node = NodeContainer::new(node); - self.store_node(node, id, path.into()); + self.store_node(node, id, path.into(), ids.into()); } }; Ok(()) diff --git a/node-graph/interpreted-executor/src/dynamic_executor/test.rs b/node-graph/interpreted-executor/src/dynamic_executor/test.rs index 58774520cef..cc4b898a75f 100644 --- a/node-graph/interpreted-executor/src/dynamic_executor/test.rs +++ b/node-graph/interpreted-executor/src/dynamic_executor/test.rs @@ -861,3 +861,40 @@ fn a_whole_list_of_scalars_switches_as_one_bundle() { let list = result.expect("The whole scalar list should round-trip through the bundle switch"); assert_eq!(list.len(), 2, "The true branch's whole list should be selected and preserved intact"); } + +fn error_traced_through(indices: &[usize]) -> NodeError { + let mut state = Poll::<()>::error("boom"); + for &index in indices { + let status = core_types::Status::new(); + let Err(interrupt) = status.record(index, state) else { unreachable!() }; + state = status.bail(interrupt); + } + let Poll::Error(error) = state else { unreachable!() }; + *error +} + +#[test] +fn a_trace_leads_to_the_node_that_raised_the_error() { + let executor = DynamicExecutor::new(bounding_box_network(TaggedValue::TypeDefault(item!(Vector)))).unwrap(); + + assert_eq!(executor.find_origin(executor.output(), &error_traced_through(&[])), Some(NodeId(1))); + assert_eq!(executor.find_origin(executor.output(), &error_traced_through(&[0])), Some(NodeId(0))); + assert_eq!(executor.find_origin(executor.output(), &error_traced_through(&[0, 0])), None, "a value node has no inputs"); + assert_eq!(executor.find_origin(NodeId(0), &error_traced_through(&[])), Some(NodeId(0))); + assert_eq!(executor.find_origin(NodeId(0), &error_traced_through(&[0])), None, "an upstream trace starts at its parent's origin"); +} + +#[test] +fn a_trace_steps_over_the_index_of_a_promotion_adapter() { + let value_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::TypeDefault(item!(f64)).into()), vec![NodeId(0)]); + let mut box_corners_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(0)]), vec![NodeId(1)]); + box_corners_node.identifier = graphene_std::vector::generator_nodes::box_corners::IDENTIFIER; + let executor = DynamicExecutor::new(ProtoNetwork { + inputs: vec![], + output: NodeId(1), + nodes: vec![(NodeId(0), value_node), (NodeId(1), box_corners_node)], + }) + .unwrap(); + + assert_eq!(executor.find_origin(executor.output(), &error_traced_through(&[0, 0])), Some(NodeId(0))); +} From eef450a6663861284ea9e2a0166e08bff67aa2a2 Mon Sep 17 00:00:00 2001 From: Timon Date: Wed, 14 Oct 2026 17:12:15 +0000 Subject: [PATCH 34/54] Show evaluation errors in the node graph --- .../document/node_graph/node_graph_message.rs | 5 +- .../node_graph/node_graph_message_handler.rs | 37 ++-- .../document/node_graph/utility_types.rs | 6 + editor/src/node_graph_executor.rs | 38 +++- editor/src/node_graph_executor/runtime.rs | 176 +++++++++--------- .../src/dynamic_executor.rs | 10 +- .../src/dynamic_executor/test.rs | 22 +++ 7 files changed, 187 insertions(+), 107 deletions(-) diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs index 4d9f6896d44..3aeed83037b 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs @@ -1,4 +1,4 @@ -use super::utility_types::Direction; +use super::utility_types::{Direction, NodeEvaluationError}; use crate::messages::input_mapper::utility_types::keyboard::Key; use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; @@ -257,6 +257,9 @@ pub enum NodeGraphMessage { #[serde(skip)] node_graph_errors: GraphErrors, }, + UpdateEvaluationErrors { + errors: Vec, + }, UpdateActionButtons, UpdateGraphBarRight, UpdateInSelectedNetwork, diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index e999a6675b8..86f16df4c81 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -9,7 +9,7 @@ use crate::messages::portfolio::document::graph_operation::utility_types::Modify use crate::messages::portfolio::document::node_graph::document_node_definitions::{ DefinitionIdentifier, NodePropertiesContext, resolve_document_node_type, resolve_network_node_type, resolve_proto_node_type, }; -use crate::messages::portfolio::document::node_graph::utility_types::{ContextMenuData, Direction, FrontendGraphDataType, NodeGraphErrorDiagnostic}; +use crate::messages::portfolio::document::node_graph::utility_types::{ContextMenuData, Direction, FrontendGraphDataType, NodeEvaluationError, NodeGraphErrorDiagnostic}; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::misc::GroupFolderType; use crate::messages::portfolio::document::utility_types::network_interface::{self, FlowType, InputConnector, NodeNetworkInterface, NodeTypePersistentMetadata, OutputConnector, Previewing}; @@ -101,6 +101,7 @@ pub struct NodeGraphMessageHandler { frontend_nodes: Vec, /// Used to keep track of what wires are sent to the front end so the old ones can be removed frontend_wires: HashSet<(NodeId, usize)>, + evaluation_errors: Vec, } /// NodeGraphMessageHandler always modifies the network which the selected nodes are in. No GraphOperationMessages should be added here, since those messages will always affect the document network. @@ -1728,6 +1729,9 @@ impl<'a> MessageHandler> for NodeG nodes.push(*node_id); } } + if self.evaluation_errors.iter().any(|error| error.node_path.contains(node_id)) { + nodes.push(*node_id); + } } responses.add(FrontendMessage::UpdateVisibleNodes { nodes }); @@ -2194,6 +2198,16 @@ impl<'a> MessageHandler> for NodeG responses.add(NodeGraphMessage::SendGraph); } + NodeGraphMessage::UpdateEvaluationErrors { errors } => { + if self.evaluation_errors != errors { + self.evaluation_errors = errors; + if breadcrumb_network_path == selection_network_path && graph_view_overlay_open { + let error = self.node_graph_error(network_interface, breadcrumb_network_path); + responses.add(FrontendMessage::UpdateNodeGraphErrorDiagnostic { error }); + responses.add(NodeGraphMessage::UpdateVisibleNodes); + } + } + } NodeGraphMessage::UpdateActionButtons => { if selection_network_path == breadcrumb_network_path { self.update_graph_bar_left(network_interface, breadcrumb_network_path, responses); @@ -2808,17 +2822,17 @@ impl NodeGraphMessageHandler { } fn node_graph_error(&self, network_interface: &mut NodeNetworkInterface, breadcrumb_network_path: &[NodeId]) -> Option { - let graph_error = network_interface - .resolved_types - .node_graph_errors - .iter() - .find(|error| error.node_path.starts_with(breadcrumb_network_path) && error.node_path.len() > breadcrumb_network_path.len())?; - let error = if graph_error.node_path.len() == breadcrumb_network_path.len() + 1 { - format!("{:?}", graph_error.error) - } else { - "Node graph type error within this node".to_string() + let depth = breadcrumb_network_path.len(); + let inside = |path: &[NodeId]| path.starts_with(breadcrumb_network_path) && path.len() > depth; + let graph_error = network_interface.resolved_types.node_graph_errors.iter().find(|error| inside(&error.node_path)); + let evaluation_error = self.evaluation_errors.iter().find(|error| inside(&error.node_path)); + let (error_node, error) = match (graph_error, evaluation_error) { + (Some(graph_error), _) if graph_error.node_path.len() == depth + 1 => (graph_error.node_path[depth], format!("{:?}", graph_error.error)), + (Some(graph_error), _) => (graph_error.node_path[depth], "Node graph type error within this node".to_string()), + (None, Some(error)) if error.node_path.len() == depth + 1 => (error.node_path[depth], error.message.clone()), + (None, Some(error)) => (error.node_path[depth], "Node graph error within this node".to_string()), + (None, None) => return None, }; - let error_node = graph_error.node_path[breadcrumb_network_path.len()]; let mut position = network_interface.position(&error_node, breadcrumb_network_path)?; // Convert to graph space @@ -2966,6 +2980,7 @@ impl Default for NodeGraphMessageHandler { end_index: None, frontend_nodes: Vec::new(), frontend_wires: HashSet::new(), + evaluation_errors: Vec::new(), } } } diff --git a/editor/src/messages/portfolio/document/node_graph/utility_types.rs b/editor/src/messages/portfolio/document/node_graph/utility_types.rs index 9b4918d4c58..78f42eb7ee9 100644 --- a/editor/src/messages/portfolio/document/node_graph/utility_types.rs +++ b/editor/src/messages/portfolio/document/node_graph/utility_types.rs @@ -183,6 +183,12 @@ pub struct NodeGraphErrorDiagnostic { pub error: String, } +#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct NodeEvaluationError { + pub node_path: Vec, + pub message: String, +} + #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] #[derive(Clone, Debug, PartialEq, Default, serde::Serialize, serde::Deserialize)] pub struct FrontendClickTargets { diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index 475dfcf88ca..0e9f6c69222 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -1,4 +1,5 @@ use crate::messages::frontend::utility_types::{ExportBounds, FileType, RasterizedImage}; +use crate::messages::portfolio::document::node_graph::utility_types::NodeEvaluationError; use crate::messages::portfolio::document::utility_types::network_interface::InputConnector; use crate::messages::prelude::*; use glam::{DAffine2, DVec2, UVec2}; @@ -34,13 +35,23 @@ pub struct ExecutionRequest { pub struct ExecutionResponse { execution_id: u64, - result: Result, + result: ExecutionResult, responses: VecDeque, vector_modify: HashMap, /// The resulting value from the temporary inspected during execution inspect_result: Option, } +#[derive(Debug)] +pub enum ExecutionResult { + Value(TaggedValue), + Fallback(TaggedValue, EvaluationError), + Error(EvaluationError), + Panic(Box), +} + +pub type EvaluationError = Box<[(Arc, Box<[NodeId]>)]>; + #[derive(serde::Serialize, serde::Deserialize)] pub struct CompilationResponse { result: Result, @@ -385,14 +396,37 @@ impl NodeGraphExecutor { // Apply only to the active document's in-progress migration, dropping responses left over from a document switch. if let Some(bake_target) = execution_context.measure_fill { if execution_context.document_id == document_id && self.gradient_migration.as_ref().is_some_and(|migration| migration.document_id == document_id) { - self.handle_gradient_measurement(document, document_id, bake_target, result.ok().and(inspect_result), responses); + let inspect_result = matches!(result, ExecutionResult::Value(_) | ExecutionResult::Fallback(..)).then_some(inspect_result).flatten(); + self.handle_gradient_measurement(document, document_id, bake_target, inspect_result, responses); } continue; } responses.add(OverlaysMessage::Draw); + if execution_context.export_config.is_none() { + let errors = match &result { + ExecutionResult::Fallback(_, errors) | ExecutionResult::Error(errors) => errors + .iter() + .map(|(error, node_path)| NodeEvaluationError { + node_path: node_path.to_vec(), + message: error.to_string(), + }) + .collect(), + ExecutionResult::Value(_) | ExecutionResult::Panic(_) => Vec::new(), + }; + responses.add(NodeGraphMessage::UpdateEvaluationErrors { errors }); + } + let node_graph_output = match result { + ExecutionResult::Value(value) | ExecutionResult::Fallback(value, _) => Ok(value), + ExecutionResult::Error(errors) => Err(errors.iter().map(|(error, _)| error.to_string()).collect::>().join("\n")), + ExecutionResult::Panic(payload) => { + let message = payload.downcast_ref::<&str>().copied().or_else(|| payload.downcast_ref::().map(String::as_str)); + Err(format!("Panicked: {}", message.unwrap_or("no message"))) + } + }; + let node_graph_output = match node_graph_output { Ok(output) => output, Err(e) => { // Clear the click targets while the graph is in an un-renderable state diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index aa725356934..1b24d28f723 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -22,7 +22,7 @@ use graphene_std::runtime::AsyncRuntime; use graphene_std::transform::RenderQuality; use graphene_std::vector::Vector; use graphene_std::vector::style::RenderMode; -use graphene_std::{Artboard, Context, Graphic, Poll}; +use graphene_std::{Artboard, Context, Graphic, NodeError, Poll}; use interpreted_executor::dynamic_executor::{DynamicExecutor, IntrospectError, ResolvedDocumentNodeTypesDelta}; use interpreted_executor::util::wrap_network_in_scope; use spin::Mutex; @@ -218,7 +218,27 @@ impl NodeRuntime { render_config.export_format = ExportFormat::Svg; } - let result = self.execute_network(render_config); + let mut result = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| self.execute_network(render_config))) { + Ok(Ok(Poll::Pending)) => continue, + Ok(Ok(Poll::Final(value) | Poll::Partial(value))) => ExecutionResult::Value(value), + Ok(Ok(Poll::Fallback(boxed))) => { + let (value, error) = *boxed; + ExecutionResult::Fallback(value, self.resolve_errors(&error)) + } + Ok(Ok(Poll::Error(error))) => ExecutionResult::Error(self.resolve_errors(&error)), + Ok(Err(error)) => ExecutionResult::Error(Box::new([(Arc::clone(NodeError::new(error).error()), Box::default())])), + Err(payload) => { + let response = ExecutionResponse { + execution_id, + result: ExecutionResult::Panic(payload), + responses: VecDeque::new(), + vector_modify: HashMap::new(), + inspect_result: None, + }; + self.sender.send(NodeGraphUpdate::ExecutionResponse(Box::new(response))).expect("Failed to send response"); + return None; + } + }; let mut responses = VecDeque::new(); // TODO: Only process monitor nodes if the graph has changed, not when only the Footprint changes if !render_config.for_eyedropper { @@ -229,35 +249,15 @@ impl NodeRuntime { // Resolve the result from the inspection by accessing the monitor node let inspect_result = self.inspect_state.as_ref().and_then(|state| state.access(&self.executor)); - let (result, texture) = match result { - Ok(TaggedValue::RenderOutput(RenderOutput { - data: RenderOutputType::Texture(texture), - metadata, - })) if render_config.for_export => { - let executor = self - .editor_api - .application_io - .as_ref() - .unwrap() - .gpu_executor() - .expect("GPU executor should be available when we receive a texture"); - - let raster_cpu = Raster::new_gpu(texture).convert(Footprint::BOUNDLESS, executor.clone()).await; - - let (data, width, height) = raster_cpu.to_flat_u8(); - - ( - Ok(TaggedValue::RenderOutput(RenderOutput { - data: RenderOutputType::Buffer { data, width, height }, - metadata, - })), - None, - ) - } - Ok(TaggedValue::RenderOutput(RenderOutput { - data: RenderOutputType::Texture(texture), - metadata: _, - })) if render_config.for_eyedropper => { + let output = match &mut result { + ExecutionResult::Value(value) | ExecutionResult::Fallback(value, _) => Some(value), + ExecutionResult::Error(_) | ExecutionResult::Panic(_) => None, + }; + let texture = if let Some(TaggedValue::RenderOutput(RenderOutput { data, .. })) = output + && let RenderOutputType::Texture(texture) = data + { + let texture = texture.clone(); + if render_config.for_export || render_config.for_eyedropper { let executor = self .editor_api .application_io @@ -268,49 +268,43 @@ impl NodeRuntime { let raster_cpu = Raster::new_gpu(texture).convert(Footprint::BOUNDLESS, executor.clone()).await; - self.sender.send(NodeGraphUpdate::EyedropperPreview(raster_cpu)).expect("Failed to send response"); - continue; + if render_config.for_eyedropper { + self.sender.send(NodeGraphUpdate::EyedropperPreview(raster_cpu)).expect("Failed to send response"); + continue; + } + + let (buffer, width, height) = raster_cpu.to_flat_u8(); + *data = RenderOutputType::Buffer { data: buffer, width, height }; + None + } else { + #[cfg(all(target_family = "wasm", feature = "gpu"))] + { + self.current_viewport_texture = Some(texture.clone()); + + let app_io = self.editor_api.application_io.as_ref().unwrap(); + let executor = app_io.gpu_executor().expect("GPU executor should be available when we receive a texture"); + + self.wasm_canvas_cache.present(&texture, executor); + + let logical_resolution = render_config.viewport.resolution.as_dvec2() / render_config.scale; + *data = RenderOutputType::CanvasFrame { + canvas_id: self.wasm_canvas_cache.id(), + resolution: logical_resolution, + }; + None + } + #[cfg(not(all(target_family = "wasm", feature = "gpu")))] + { + Some(texture) + } } - // Eyedropper render that didn't produce a texture (e.g., SVG fallback when GPU is unavailable); discard it - _ if render_config.for_eyedropper => { - continue; - } - #[cfg(all(target_family = "wasm", feature = "gpu"))] - Ok(TaggedValue::RenderOutput(RenderOutput { - data: RenderOutputType::Texture(texture), - metadata, - })) if !render_config.for_export => { - self.current_viewport_texture = Some(texture.clone()); - - let app_io = self.editor_api.application_io.as_ref().unwrap(); - let executor = app_io.gpu_executor().expect("GPU executor should be available when we receive a texture"); - - self.wasm_canvas_cache.present(&texture, executor); - - let logical_resolution = render_config.viewport.resolution.as_dvec2() / render_config.scale; - ( - Ok(TaggedValue::RenderOutput(RenderOutput { - data: RenderOutputType::CanvasFrame { - canvas_id: self.wasm_canvas_cache.id(), - resolution: logical_resolution, - }, - metadata, - })), - None, - ) - } - Ok(TaggedValue::RenderOutput(RenderOutput { - data: RenderOutputType::Texture(texture), - metadata, - })) => ( - Ok(TaggedValue::RenderOutput(RenderOutput { - data: RenderOutputType::Texture(texture.clone()), - metadata, - })), - Some(texture), - ), - r => (r, None), + } else { + None }; + // Eyedropper render that didn't produce a texture (e.g., SVG fallback when GPU is unavailable); discard it + if render_config.for_eyedropper { + continue; + } let response = ExecutionResponse { execution_id, @@ -358,26 +352,36 @@ impl NodeRuntime { result } - fn execute_network(&mut self, render_config: RenderConfig) -> Result { + fn execute_network(&mut self, render_config: RenderConfig) -> Result, String> { use graph_craft::graphene_compiler::Executor; - let state = match self.executor.input_type() { + match self.executor.input_type() { Some(t) if t == concrete!(Context) => (&self.executor) .execute(render_config.into_context_with_generations(self.async_runtime().snapshot())) - .map_err(|e| e.to_string())?, - Some(t) if t == concrete!(()) => (&self.executor).execute(()).map_err(|e| e.to_string())?, - Some(t) => return Err(format!("Invalid input type {t:?}")), - _ => return Err(format!("No input type:\n{:?}", self.node_graph_errors)), - }; - match state { - Poll::Final(value) | Poll::Partial(value) => Ok(value), - Poll::Fallback(boxed) => { - let (value, error) = *boxed; - Ok(value) + .map_err(|e| e.to_string()), + Some(t) if t == concrete!(()) => (&self.executor).execute(()).map_err(|e| e.to_string()), + Some(t) => Err(format!("Invalid input type {t:?}")), + _ => Err(format!("No input type:\n{:?}", self.node_graph_errors)), + } + } + + fn resolve_errors(&self, error: &NodeError) -> EvaluationError { + let source_map = self.executor.tree().source_map(); + let mut errors = Vec::new(); + let mut pending = vec![(Some(self.executor.output()), error)]; + while let Some((node, error)) = pending.pop() { + let origin = node.and_then(|node| self.executor.find_origin(node, error)); + let start = errors.len(); + if let Some(origin) = origin { + errors.extend(source_map.iter().filter(|(_, (node, _))| *node == origin).map(|(path, _)| (Arc::clone(error.error()), path.clone()))); + errors[start..].sort_unstable_by(|(_, a), (_, b)| a.cmp(b)); + } + if errors.len() == start { + errors.push((Arc::clone(error.error()), Box::default())); } - Poll::Pending => Err("Node graph evaluation is pending".to_string()), - Poll::Error(error) => Err(format!("Node graph evaluation failed: {error}")), + pending.extend(error.upstream().iter().rev().map(|upstream| (origin, upstream))); } + errors.into_boxed_slice() } /// Updates state data diff --git a/node-graph/interpreted-executor/src/dynamic_executor.rs b/node-graph/interpreted-executor/src/dynamic_executor.rs index 6a3e407e3b8..aa79de34921 100644 --- a/node-graph/interpreted-executor/src/dynamic_executor.rs +++ b/node-graph/interpreted-executor/src/dynamic_executor.rs @@ -162,15 +162,10 @@ where I: 'static + WasmNotSend, { fn execute(&self, input: I) -> Result, Box> { - match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| self.tree.eval_tagged_value(self.output, &input))) { - Ok(result) => result.map_err(|e| e.into()), - Err(e) => { - Box::leak(e); - Err("Node graph execution panicked".into()) - } - } + self.tree.eval_tagged_value(self.output, &input).map_err(|error| error.into()) } } + pub struct InputMapping {} #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -272,6 +267,7 @@ impl BorrowTree { let (node, _path, _) = self.nodes.get(&id)?; node.eval(input).value().and_then(|output| output.downcast::().ok().map(|o| *o)) } + /// Evaluate the output node of the [`BorrowTree`] and cast it to a tagged value. /// This ensures that no borrowed data can escape the node graph. pub fn eval_tagged_value(&self, id: NodeId, input: &I) -> Result, String> diff --git a/node-graph/interpreted-executor/src/dynamic_executor/test.rs b/node-graph/interpreted-executor/src/dynamic_executor/test.rs index cc4b898a75f..e2dc7f161ae 100644 --- a/node-graph/interpreted-executor/src/dynamic_executor/test.rs +++ b/node-graph/interpreted-executor/src/dynamic_executor/test.rs @@ -898,3 +898,25 @@ fn a_trace_steps_over_the_index_of_a_promotion_adapter() { assert_eq!(executor.find_origin(executor.output(), &error_traced_through(&[0, 0])), Some(NodeId(0))); } + +#[test] +fn executor_panics_reach_the_host_with_the_original_payload() { + struct Panics; + impl core_types::Node for Panics { + type Output = core_types::registry::Any; + fn eval(&self, _: &core_types::registry::AnyInput) -> Poll { + std::panic::panic_any(42_u32); + } + } + + let mut executor = DynamicExecutor { + output: NodeId(0), + ..Default::default() + }; + let missing_output = (&executor).execute(()).unwrap_err(); + assert_eq!(missing_output.to_string(), "Output node not found in executor"); + + executor.tree.store_node(NodeContainer::new(Box::new(Panics)), NodeId(0), Box::default(), Box::default()); + let payload = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| (&executor).execute(()))).unwrap_err(); + assert_eq!(*payload.downcast::().unwrap(), 42); +} From 0bae066fc1ff34ba1e1e370d4290a71679612f65 Mon Sep 17 00:00:00 2001 From: Timon Date: Tue, 13 Oct 2026 02:30:16 +0000 Subject: [PATCH 35/54] Default unmapped item primaries to a value --- node-graph/node-macro/src/codegen.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node-graph/node-macro/src/codegen.rs b/node-graph/node-macro/src/codegen.rs index 1d975ec78ab..ba533ea9530 100644 --- a/node-graph/node-macro/src/codegen.rs +++ b/node-graph/node-macro/src/codegen.rs @@ -243,7 +243,7 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn }; match implementations.first() { // A primary's scalar `#[default]` parses as a bare element (unranked, promoted at resolution); without one it defaults to an empty List - Some(implementation_ty) if Some(index) == primary_regular_index && element_wise => match value_source { + Some(implementation_ty) if Some(index) == primary_regular_index && element_wise && mapped_variant => match value_source { ParsedValueSource::Default(_) => quote!(Some(concrete!(#implementation_ty))), _ => quote!(Some(#core_types::list!(#implementation_ty))), }, From cd389d5d45c804f679ef7d263907bf1b9eb908fb Mon Sep 17 00:00:00 2001 From: Timon Date: Wed, 14 Oct 2026 17:12:16 +0000 Subject: [PATCH 36/54] Add debug nodes --- node-graph/nodes/gstd/src/lib.rs | 1 + node-graph/nodes/gstd/src/poll_debug.rs | 131 ++++++++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 node-graph/nodes/gstd/src/poll_debug.rs diff --git a/node-graph/nodes/gstd/src/lib.rs b/node-graph/nodes/gstd/src/lib.rs index 4090049bd11..a1ce80165ec 100644 --- a/node-graph/nodes/gstd/src/lib.rs +++ b/node-graph/nodes/gstd/src/lib.rs @@ -1,5 +1,6 @@ pub mod any; pub mod platform_application_io; +pub mod poll_debug; pub mod render_background; pub mod render_cache; pub mod render_node; diff --git a/node-graph/nodes/gstd/src/poll_debug.rs b/node-graph/nodes/gstd/src/poll_debug.rs new file mode 100644 index 00000000000..cd40cb3c3d9 --- /dev/null +++ b/node-graph/nodes/gstd/src/poll_debug.rs @@ -0,0 +1,131 @@ +use core_types::list::Item; +use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, ExtractGenerations, Interrupt, NodeError, OwnedContextImpl, Poll}; +use graphic_types::{Graphic, Vector}; +use std::sync::Arc; +use std::sync::atomic::{AtomicU64, Ordering}; + +#[node_macro::node(category("Debug"), placeholder = pending_delay)] +async fn delay( + _: impl Ctx, + #[implementations(f64, String, Vector, Graphic)] value: Item, + #[default(1.)] + #[unit(" sec")] + seconds: Item, +) -> Item { + sleep(*seconds.element()).await; + value +} + +fn pending_delay(_value: &Item, _seconds: &Item) -> Item { + Item::default() +} + +#[node_macro::node(category("Debug"))] +async fn hold( + _: impl Ctx, + #[implementations(f64, String, Vector, Graphic)] value: Item, + #[default(1.)] + #[unit(" sec")] + seconds: Item, +) -> Item { + sleep(*seconds.element()).await; + value +} + +#[node_macro::node(category("Debug"))] +fn error(_: impl Ctx, #[implementations(f64, String, Vector, Graphic)] value: Item, #[default(true)] fail: Item) -> Result, Interrupt> { + match *fail.element() { + true => Err(NodeError::new("debug error").into()), + false => Ok(value), + } +} + +#[node_macro::node(category("Debug"))] +fn fallback(_: impl Ctx, #[implementations(f64, String, Vector, Graphic)] value: Item, #[default(true)] fail: Item) -> Result, (Item, NodeError)> { + match *fail.element() { + true => Err((Item::default(), NodeError::new("debug fallback"))), + false => Ok(value), + } +} + +#[node_macro::node(category("Debug"), name("Panic"))] +fn unwind(_: impl Ctx, #[implementations(f64, String, Vector, Graphic)] value: Item, fail: Item) -> Item { + if *fail.element() { + panic!("debug panic"); + } + value +} + +#[node_macro::node(category("Debug"))] +fn evaluation_count(_: impl Ctx, #[implementations(f64, String, Vector, Graphic)] value: Item, #[data] count: Arc) -> Item { + let count = count.fetch_add(1, Ordering::Relaxed) + 1; + log::info!("Debug evaluation count: {count}"); + value +} + +#[node_macro::node(category("Debug"))] +fn generations(ctx: impl Ctx + ExtractGenerations, value: Item) -> Item { + let generations = ctx + .try_generations() + .map(|generations| generations.iter().map(|(source, generation)| format!("{:x}={generation}", source.0)).collect::>().join(", ")) + .unwrap_or_default(); + Item::new_from_element(format!("{} [{generations}]", value.element())) +} + +#[node_macro::node(category("Debug"), name("Stand-In"))] +fn stand_in( + ctx: impl Ctx + ExtractAll + CloneVarArgs, + #[expose] + #[implementations(Context -> Item, Context -> Item, Context -> Item, Context -> Item)] + content: impl Node, Output = T>, + #[implementations(Item, Item, Item, Item)] placeholder: T, +) -> Poll { + match content.eval(&OwnedContextImpl::from(ctx).into_context()) { + Poll::Pending | Poll::Partial(_) => Poll::Partial(placeholder), + finished => finished, + } +} + +#[cfg(not(target_family = "wasm"))] +async fn sleep(seconds: f64) { + use std::sync::Mutex; + use std::task::{Poll, Waker}; + + #[derive(Default)] + struct Timer(Arc)>>); + + impl Future for Timer { + type Output = (); + fn poll(self: std::pin::Pin<&mut Self>, context: &mut std::task::Context<'_>) -> Poll<()> { + let mut state = self.0.lock().unwrap(); + if state.0 { + return Poll::Ready(()); + } + state.1 = Some(context.waker().clone()); + Poll::Pending + } + } + + let timer = Timer::default(); + let state = Arc::clone(&timer.0); + std::thread::spawn(move || { + std::thread::sleep(std::time::Duration::from_secs_f64(seconds.max(0.))); + let mut state = state.lock().unwrap(); + state.0 = true; + if let Some(waker) = state.1.take() { + waker.wake(); + } + }); + timer.await +} + +#[cfg(target_family = "wasm")] +async fn sleep(seconds: f64) { + let promise = web_sys::js_sys::Promise::new(&mut |resolve, _| { + let window = web_sys::window().expect("a window exists on the web"); + window + .set_timeout_with_callback_and_timeout_and_arguments_0(&resolve, (seconds.max(0.) * 1000.) as i32) + .expect("setTimeout accepts a function"); + }); + let _ = wasm_bindgen_futures::JsFuture::from(promise).await; +} From bb7461cba926efcc2d766553ba6323b448cf73ff Mon Sep 17 00:00:00 2001 From: Timon Date: Thu, 15 Oct 2026 15:05:13 +0000 Subject: [PATCH 37/54] Memoize fallback results too --- node-graph/nodes/gcore/src/memo.rs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/node-graph/nodes/gcore/src/memo.rs b/node-graph/nodes/gcore/src/memo.rs index edf6f9cee1f..fed99eaffa1 100644 --- a/node-graph/nodes/gcore/src/memo.rs +++ b/node-graph/nodes/gcore/src/memo.rs @@ -28,7 +28,7 @@ fn memoize(input: I, conten } let state = content.eval(&input); - if matches!(state, Poll::Final(_) | Poll::Partial(_)) { + if !matches!(state, Poll::Pending | Poll::Error(_)) { *cache.lock().unwrap() = Some((hash, state.clone())); } state @@ -59,6 +59,7 @@ fn serialize_monitor); + + impl Node<()> for StandIn { + type Output = u32; + fn eval(&self, _: &()) -> Poll { + self.0.fetch_add(1, Ordering::Relaxed); + Poll::fallback(1, "stand-in") + } + } + + #[test] + fn a_fallback_is_memoized_with_its_error() { + let evaluations = Arc::new(AtomicU32::new(0)); + let memo = MemoizeNode::new(StandIn(evaluations.clone())); + + let first = memo.eval(&()); + assert!(matches!(first, Poll::Fallback(_))); + assert_eq!(memo.eval(&()), first); + assert_eq!(evaluations.load(Ordering::Relaxed), 1); + } } From 913ac42c6e72e3d333556b72050dcbd0d8fe999d Mon Sep 17 00:00:00 2001 From: Timon Date: Wed, 14 Oct 2026 22:53:59 +0000 Subject: [PATCH 38/54] Show multiple node graph errors --- .../src/messages/frontend/frontend_message.rs | 4 +- .../node_graph/node_graph_message_handler.rs | 63 ++++++++++++------- editor/src/test_utils.rs | 6 +- frontend/src/components/views/Graph.svelte | 16 ++--- frontend/src/stores/node-graph.ts | 10 +-- 5 files changed, 60 insertions(+), 39 deletions(-) diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index 454deb336e6..50336924286 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -271,8 +271,8 @@ pub enum FrontendMessage { UpdateNodeGraphNodes { nodes: Vec, }, - UpdateNodeGraphErrorDiagnostic { - error: Option, + UpdateNodeGraphErrorDiagnostics { + errors: Vec, }, UpdateVisibleNodes { nodes: Vec, diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index 86f16df4c81..a0d55f58136 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -1747,8 +1747,8 @@ impl<'a> MessageHandler> for NodeG responses.add(FrontendMessage::UpdateNodeGraphNodes { nodes }); responses.add(NodeGraphMessage::UpdateVisibleNodes); - let error = self.node_graph_error(network_interface, breadcrumb_network_path); - responses.add(FrontendMessage::UpdateNodeGraphErrorDiagnostic { error }); + let errors = self.node_graph_errors(network_interface, breadcrumb_network_path); + responses.add(FrontendMessage::UpdateNodeGraphErrorDiagnostics { errors }); let (layer_widths, chain_widths, has_left_input_wire) = network_interface.collect_layer_widths(breadcrumb_network_path); responses.add(NodeGraphMessage::UpdateImportsExports); @@ -2202,8 +2202,8 @@ impl<'a> MessageHandler> for NodeG if self.evaluation_errors != errors { self.evaluation_errors = errors; if breadcrumb_network_path == selection_network_path && graph_view_overlay_open { - let error = self.node_graph_error(network_interface, breadcrumb_network_path); - responses.add(FrontendMessage::UpdateNodeGraphErrorDiagnostic { error }); + let errors = self.node_graph_errors(network_interface, breadcrumb_network_path); + responses.add(FrontendMessage::UpdateNodeGraphErrorDiagnostics { errors }); responses.add(NodeGraphMessage::UpdateVisibleNodes); } } @@ -2821,28 +2821,47 @@ impl NodeGraphMessageHandler { Some(subgraph_names) } - fn node_graph_error(&self, network_interface: &mut NodeNetworkInterface, breadcrumb_network_path: &[NodeId]) -> Option { + fn node_graph_errors(&self, network_interface: &mut NodeNetworkInterface, breadcrumb_network_path: &[NodeId]) -> Vec { let depth = breadcrumb_network_path.len(); let inside = |path: &[NodeId]| path.starts_with(breadcrumb_network_path) && path.len() > depth; - let graph_error = network_interface.resolved_types.node_graph_errors.iter().find(|error| inside(&error.node_path)); - let evaluation_error = self.evaluation_errors.iter().find(|error| inside(&error.node_path)); - let (error_node, error) = match (graph_error, evaluation_error) { - (Some(graph_error), _) if graph_error.node_path.len() == depth + 1 => (graph_error.node_path[depth], format!("{:?}", graph_error.error)), - (Some(graph_error), _) => (graph_error.node_path[depth], "Node graph type error within this node".to_string()), - (None, Some(error)) if error.node_path.len() == depth + 1 => (error.node_path[depth], error.message.clone()), - (None, Some(error)) => (error.node_path[depth], "Node graph error within this node".to_string()), - (None, None) => return None, + let graph_errors: Vec<_> = network_interface + .resolved_types + .node_graph_errors + .iter() + .filter(|error| inside(&error.node_path)) + .map(|error| match error.node_path.len() == depth + 1 { + true => (error.node_path[depth], format!("{:?}", error.error)), + false => (error.node_path[depth], "Node graph type error within this node".to_string()), + }) + .collect(); + // Evaluation errors come from the last graph that compiled, so a type error hides them + let errors = match graph_errors.is_empty() { + true => self + .evaluation_errors + .iter() + .filter(|error| inside(&error.node_path)) + .map(|error| match error.node_path.len() == depth + 1 { + true => (error.node_path[depth], error.message.clone()), + false => (error.node_path[depth], "Node graph error within this node".to_string()), + }) + .collect(), + false => graph_errors, }; - let mut position = network_interface.position(&error_node, breadcrumb_network_path)?; - // Convert to graph space - position *= 24; - if network_interface.is_layer(&error_node, breadcrumb_network_path) { - position += IVec2::new(12, -12) - } - let position = position.into(); - - Some(NodeGraphErrorDiagnostic { position, error }) + let mut shown = HashSet::new(); + errors + .into_iter() + .filter(|(node, _)| shown.insert(*node)) + .filter_map(|(node, error)| { + let mut position = network_interface.position(&node, breadcrumb_network_path)?; + // Convert to graph space + position *= 24; + if network_interface.is_layer(&node, breadcrumb_network_path) { + position += IVec2::new(12, -12) + } + Some(NodeGraphErrorDiagnostic { position: position.into(), error }) + }) + .collect() } fn update_layer_panel(network_interface: &NodeNetworkInterface, selection_network_path: &[NodeId], layers_panel_open: bool, responses: &mut VecDeque) { diff --git a/editor/src/test_utils.rs b/editor/src/test_utils.rs index d17b0a3310f..532ff6d24e7 100644 --- a/editor/src/test_utils.rs +++ b/editor/src/test_utils.rs @@ -340,9 +340,9 @@ pub trait FrontendMessageTestUtils { impl FrontendMessageTestUtils for FrontendMessage { fn check_node_graph_error(&self) { - let FrontendMessage::UpdateNodeGraphErrorDiagnostic { error } = self else { return }; - if let Some(error) = error { - panic!("error: {:?}", error); + let FrontendMessage::UpdateNodeGraphErrorDiagnostics { errors } = self else { return }; + if !errors.is_empty() { + panic!("errors: {errors:?}"); } } } diff --git a/frontend/src/components/views/Graph.svelte b/frontend/src/components/views/Graph.svelte index 66172406ffd..32354c5405a 100644 --- a/frontend/src/components/views/Graph.svelte +++ b/frontend/src/components/views/Graph.svelte @@ -281,14 +281,16 @@ {/if} - {#if $nodeGraph.error} + {#if $nodeGraph.errors.length > 0}

- - {$nodeGraph.error.error} - - - {$nodeGraph.error.error} - + {#each $nodeGraph.errors as error} + + {error.error} + + + {error.error} + + {/each}
{/if} diff --git a/frontend/src/stores/node-graph.ts b/frontend/src/stores/node-graph.ts index 24d7c7e796c..5b7595d8daa 100644 --- a/frontend/src/stores/node-graph.ts +++ b/frontend/src/stores/node-graph.ts @@ -12,7 +12,7 @@ type NodeGraphStoreState = { box: BoxSelection | undefined; clickTargets: FrontendClickTargets | undefined; contextMenuInformation: ContextMenuInformation | undefined; - error: NodeGraphErrorDiagnostic | undefined; + errors: NodeGraphErrorDiagnostic[]; layerWidths: Map; chainWidths: Map; hasLeftInputWire: Map; @@ -30,7 +30,7 @@ const initialState: NodeGraphStoreState = { box: undefined, clickTargets: undefined, contextMenuInformation: undefined, - error: undefined, + errors: [], layerWidths: new Map(), chainWidths: new Map(), hasLeftInputWire: new Map(), @@ -146,9 +146,9 @@ export function createNodeGraphStore(subscriptions: SubscriptionsRouter) { }); }); - subscriptions.subscribeFrontendMessage("UpdateNodeGraphErrorDiagnostic", (data) => { + subscriptions.subscribeFrontendMessage("UpdateNodeGraphErrorDiagnostics", (data) => { update((state) => { - state.error = data.error; + state.errors = data.errors; return state; }); }); @@ -240,7 +240,7 @@ export function destroyNodeGraphStore() { subscriptions.unsubscribeFrontendMessage("UpdateInSelectedNetwork"); subscriptions.unsubscribeFrontendMessage("UpdateLayerWidths"); subscriptions.unsubscribeFrontendMessage("UpdateNodeGraphNodes"); - subscriptions.unsubscribeFrontendMessage("UpdateNodeGraphErrorDiagnostic"); + subscriptions.unsubscribeFrontendMessage("UpdateNodeGraphErrorDiagnostics"); subscriptions.unsubscribeFrontendMessage("UpdateVisibleNodes"); subscriptions.unsubscribeFrontendMessage("UpdateNodeGraphWires"); subscriptions.unsubscribeFrontendMessage("ClearAllNodeGraphWires"); From 918ea18b1e72f279e321310df31676fdfb85a39f Mon Sep 17 00:00:00 2001 From: Timon Date: Wed, 14 Oct 2026 21:12:41 +0000 Subject: [PATCH 39/54] Report node errors in CLI --- node-graph/graphene-cli/src/export.rs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/node-graph/graphene-cli/src/export.rs b/node-graph/graphene-cli/src/export.rs index 255d2839d3a..58aa08a83f8 100644 --- a/node-graph/graphene-cli/src/export.rs +++ b/node-graph/graphene-cli/src/export.rs @@ -6,6 +6,7 @@ use graphene_std::core_types::ops::ConvertAsync; use graphene_std::core_types::transform::Footprint; use graphene_std::raster_types::{CPU, GPU, Raster}; use graphene_std::runtime::AsyncRuntime; +use graphene_std::{NodeError, Poll}; use interpreted_executor::dynamic_executor::DynamicExecutor; use std::error::Error; use std::io::Cursor; @@ -243,5 +244,22 @@ async fn execute_until_settled(executor: &DynamicExecutor, runtime: &AsyncRuntim while spawner.join().await { frame = execute()?; } - frame.value().ok_or_else(|| "The node graph produced no frame".into()) + match frame { + Poll::Fallback(boxed) => { + let (value, error) = *boxed; + log::warn!("Exported fallback value due to node error: {}", messages(&error)); + Ok(value) + } + Poll::Error(error) => Err(format!("Node graph failed: {}", messages(&error)).into()), + frame => frame.value().ok_or_else(|| "Node graph produced no frame".into()), + } +} + +fn messages(error: &NodeError) -> String { + let mut text = error.to_string(); + for upstream in error.upstream() { + text.push('\n'); + text.push_str(&messages(upstream)); + } + text } From b728304a805226e7f3cd5c5259ac3b6f29819b7c Mon Sep 17 00:00:00 2001 From: Timon Date: Wed, 14 Oct 2026 22:22:28 +0000 Subject: [PATCH 40/54] Attach input fallback error as upstream --- .../src/dynamic_executor/test.rs | 27 ++++++++++++++ node-graph/libraries/core-types/src/poll.rs | 35 ++++++++++++++++--- node-graph/node-macro/src/codegen.rs | 4 +-- 3 files changed, 60 insertions(+), 6 deletions(-) diff --git a/node-graph/interpreted-executor/src/dynamic_executor/test.rs b/node-graph/interpreted-executor/src/dynamic_executor/test.rs index e2dc7f161ae..ee975ea46a8 100644 --- a/node-graph/interpreted-executor/src/dynamic_executor/test.rs +++ b/node-graph/interpreted-executor/src/dynamic_executor/test.rs @@ -920,3 +920,30 @@ fn executor_panics_reach_the_host_with_the_original_payload() { let payload = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| (&executor).execute(()))).unwrap_err(); assert_eq!(*payload.downcast::().unwrap(), 42); } + +#[test] +fn chained_fallbacks_both_reach_the_root() { + let value_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::Number(1.).into()), vec![NodeId(0)]); + let fail_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::Bool(true).into()), vec![NodeId(1)]); + let mut first = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(0), NodeId(1)]), vec![NodeId(2)]); + first.identifier = graphene_std::poll_debug::fallback::IDENTIFIER; + let mut second = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(2), NodeId(1)]), vec![NodeId(3)]); + second.identifier = graphene_std::poll_debug::fallback::IDENTIFIER; + let executor = DynamicExecutor::new(ProtoNetwork { + inputs: vec![], + output: NodeId(3), + nodes: vec![(NodeId(0), value_node), (NodeId(1), fail_node), (NodeId(2), first), (NodeId(3), second)], + }) + .unwrap(); + + let context: Context = None; + let Ok(Poll::Fallback(boxed)) = (&executor).execute(context) else { + panic!("Both nodes fall back, so the root should too") + }; + let error = &boxed.1; + assert_eq!(executor.find_origin(executor.output(), error), Some(NodeId(3))); + let [upstream] = error.upstream() else { + panic!("The second node's error should carry the first node's error") + }; + assert_eq!(executor.find_origin(NodeId(3), upstream), Some(NodeId(2))); +} diff --git a/node-graph/libraries/core-types/src/poll.rs b/node-graph/libraries/core-types/src/poll.rs index 08b3cc0f908..cf7da006bd4 100644 --- a/node-graph/libraries/core-types/src/poll.rs +++ b/node-graph/libraries/core-types/src/poll.rs @@ -134,6 +134,7 @@ pub enum Finality { pub struct Status { finality: Cell, error: Cell>, + raised: Cell>, interrupt: Cell>, no_partial: bool, } @@ -179,6 +180,11 @@ impl Status { self.error.set(first.or(Some(error))); } + pub fn raise(&self, error: NodeError) { + let first = self.raised.take(); + self.raised.set(first.or(Some(error))); + } + #[inline(always)] pub fn run(self, kernel: impl FnOnce(&Self) -> Result) -> Poll { match kernel(&self) { @@ -189,10 +195,14 @@ impl Status { #[inline(always)] pub fn finish(self, value: T) -> Poll { - match (self.finality.get(), self.error.take()) { - (Finality::Partial, _) => Poll::Partial(value), - (Finality::Final, Some(error)) => Poll::Fallback(Box::new((value, error))), - (Finality::Final, None) => Poll::Final(value), + match (self.finality.get(), self.raised.take(), self.error.take()) { + (Finality::Partial, _, _) => Poll::Partial(value), + (Finality::Final, Some(mut raised), input) => { + raised.upstream.extend(input); + Poll::Fallback(Box::new((value, raised))) + } + (Finality::Final, None, Some(error)) => Poll::Fallback(Box::new((value, error))), + (Finality::Final, None, None) => Poll::Final(value), } } @@ -353,6 +363,23 @@ mod tests { assert_eq!(error.to_string(), "invalid digit found in string"); } + #[test] + fn a_raised_error_carries_the_input_error_upstream() { + let Poll::Fallback(boxed) = Status::new().run(|cell| { + let value = cell.record(0, Poll::fallback(1, "input"))?; + cell.raise(NodeError::new("own")); + Ok(value) + }) else { + unreachable!() + }; + let error = boxed.1; + assert_eq!(error.to_string(), "own"); + assert!(error.trace().is_empty()); + let [upstream] = error.upstream() else { unreachable!() }; + assert_eq!(upstream.to_string(), "input"); + assert_eq!(upstream.trace(), [0]); + } + #[test] fn an_unrecorded_input_does_not_touch_the_status() { let switch = |take_first: bool| { diff --git a/node-graph/node-macro/src/codegen.rs b/node-graph/node-macro/src/codegen.rs index ba533ea9530..94d30b12f80 100644 --- a/node-graph/node-macro/src/codegen.rs +++ b/node-graph/node-macro/src/codegen.rs @@ -555,7 +555,7 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn match #call { Ok(value) => Ok(value), Err((value, error)) => { - __cell.fail(error); + __cell.raise(error); Ok(value) } } @@ -569,7 +569,7 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn match #call { Ok(value) => value, Err((value, error)) => { - __cell.fail(error); + __cell.raise(error); value } } From ea2b9e51058374f05c631a8f0d31d604539dd04b Mon Sep 17 00:00:00 2001 From: Timon Date: Thu, 15 Oct 2026 16:05:23 +0000 Subject: [PATCH 41/54] Keep input fallback error when node fails --- .../src/dynamic_executor/test.rs | 26 ++++++++++++++++++ node-graph/libraries/core-types/src/poll.rs | 27 ++++++++++++++++++- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/node-graph/interpreted-executor/src/dynamic_executor/test.rs b/node-graph/interpreted-executor/src/dynamic_executor/test.rs index ee975ea46a8..bf63017d8eb 100644 --- a/node-graph/interpreted-executor/src/dynamic_executor/test.rs +++ b/node-graph/interpreted-executor/src/dynamic_executor/test.rs @@ -947,3 +947,29 @@ fn chained_fallbacks_both_reach_the_root() { }; assert_eq!(executor.find_origin(NodeId(3), upstream), Some(NodeId(2))); } + +#[test] +fn an_error_after_a_fallback_keeps_both() { + let value_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::Number(1.).into()), vec![NodeId(0)]); + let fail_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::Bool(true).into()), vec![NodeId(1)]); + let mut fallback = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(0), NodeId(1)]), vec![NodeId(2)]); + fallback.identifier = graphene_std::poll_debug::fallback::IDENTIFIER; + let mut error = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(2), NodeId(1)]), vec![NodeId(3)]); + error.identifier = graphene_std::poll_debug::error::IDENTIFIER; + let executor = DynamicExecutor::new(ProtoNetwork { + inputs: vec![], + output: NodeId(3), + nodes: vec![(NodeId(0), value_node), (NodeId(1), fail_node), (NodeId(2), fallback), (NodeId(3), error)], + }) + .unwrap(); + + let context: Context = None; + let Ok(Poll::Error(error)) = (&executor).execute(context) else { + panic!("The failing node should stop the frame") + }; + assert_eq!(executor.find_origin(executor.output(), &error), Some(NodeId(3))); + let [upstream] = error.upstream() else { + panic!("The failing node's error should carry the fallback before it") + }; + assert_eq!(executor.find_origin(NodeId(3), upstream), Some(NodeId(2))); +} diff --git a/node-graph/libraries/core-types/src/poll.rs b/node-graph/libraries/core-types/src/poll.rs index cf7da006bd4..8567c2947e3 100644 --- a/node-graph/libraries/core-types/src/poll.rs +++ b/node-graph/libraries/core-types/src/poll.rs @@ -230,7 +230,13 @@ impl Status { match (self.interrupt.take(), interrupt) { (Some(Interrupt::Error(error)), _) => Poll::Error(error), (_, Interrupt::Error(_)) if self.finality.get() == Finality::Partial => Poll::Pending, - (_, interrupt) => interrupt.into(), + (_, Interrupt::Error(mut error)) => { + if error.trace.is_empty() { + error.upstream.extend(self.error.take()); + } + Poll::Error(error) + } + (_, Interrupt::Pending) => Poll::Pending, } } @@ -380,6 +386,25 @@ mod tests { assert_eq!(upstream.trace(), [0]); } + #[test] + fn a_failing_node_carries_the_input_error_upstream() { + let Poll::Error(error) = Status::new().run(|cell| -> Result { + cell.record(0, Poll::fallback(1, "input"))?; + Err(NodeError::new("own").into()) + }) else { + unreachable!() + }; + assert_eq!(error.to_string(), "own"); + let [upstream] = error.upstream() else { unreachable!() }; + assert_eq!(upstream.to_string(), "input"); + assert_eq!(upstream.trace(), [0]); + + let Poll::Error(error) = add(Poll::fallback(1, "first"), Poll::error("second")) else { + unreachable!() + }; + assert!(error.upstream().is_empty(), "an input's error is passed on, not raised"); + } + #[test] fn an_unrecorded_input_does_not_touch_the_status() { let switch = |take_first: bool| { From f818fbd8a11d2e933626f52c7270ba4a217ff709 Mon Sep 17 00:00:00 2001 From: Timon Date: Thu, 15 Oct 2026 15:21:52 +0000 Subject: [PATCH 42/54] Keep Poll status in render cache --- node-graph/node-macro/src/codegen.rs | 12 ++- node-graph/nodes/gstd/src/render_cache.rs | 104 ++++++++++++---------- 2 files changed, 66 insertions(+), 50 deletions(-) diff --git a/node-graph/node-macro/src/codegen.rs b/node-graph/node-macro/src/codegen.rs index 94d30b12f80..34286f2e30b 100644 --- a/node-graph/node-macro/src/codegen.rs +++ b/node-graph/node-macro/src/codegen.rs @@ -142,9 +142,6 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let tier = output_tier(output_type); let output = output_payload(output_type); - if tier == NodeOutputTier::Poll && (mapped_variant || list_content_variant) { - return Err(Error::new_spanned(fn_name, "An element-wise node cannot return `Poll`")); - } let slots_field = parsed.is_async.then(|| quote! { pub(super) slots: #core_types::runtime::Slots<#output> }); let struct_fields: Vec<_> = data_field_defs.chain(regular_field_defs).chain(slots_field).collect(); @@ -1182,6 +1179,11 @@ fn generates_mapped_variant(parsed: &ParsedNodeFn) -> bool { if parsed.is_async { return false; } + + if output_tier(&parsed.output_type) == NodeOutputTier::Poll { + return false; + } + // A `()` generator frames over its ranked params exactly like a lazy primary, but generating fresh content per slot instead of transforming an upstream item if is_generator_frame(parsed) { return true; @@ -1224,7 +1226,9 @@ fn is_unit_type(ty: &syn::Type) -> bool { /// Whether the element-wise node gets a list-content `List` wire variant: only a lazy primary connector qualifies, since it draws its frame from the whole content `List` (an eager primary already maps over `List` content via the mapped variant). fn generates_list_content_variant(parsed: &ParsedNodeFn) -> bool { - primary_item_element(parsed).is_some() && matches!(parsed.primary_input_field().map(|(_, field)| &field.ty), Some(ParsedFieldType::Node(_))) + !(output_tier(&parsed.output_type) == NodeOutputTier::Poll) + && primary_item_element(parsed).is_some() + && matches!(parsed.primary_input_field().map(|(_, field)| &field.ty), Some(ParsedFieldType::Node(_))) } fn generate_register_node_impl( diff --git a/node-graph/nodes/gstd/src/render_cache.rs b/node-graph/nodes/gstd/src/render_cache.rs index 25991047af5..45e6695f4a4 100644 --- a/node-graph/nodes/gstd/src/render_cache.rs +++ b/node-graph/nodes/gstd/src/render_cache.rs @@ -3,11 +3,10 @@ use core_types::list::Item; use core_types::math::bbox::AxisAlignedBbox; use core_types::transform::{Footprint, RenderQuality, Transform}; -use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime, Generations, Interrupt, OwnedContextImpl}; +use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime, Generations, OwnedContextImpl, Poll}; use glam::{DAffine2, DVec2, IVec2, UVec2}; use graph_craft::application_io::PlatformEditorApi; use graph_craft::document::value::{RenderOutput, RenderOutputType}; -use graphene_application_io::Texture; use rendering::{RenderOutputType as RenderOutputTypeRequest, RenderParams}; use std::collections::HashSet; use std::hash::Hash; @@ -26,10 +25,9 @@ pub struct TileCoord { #[derive(Debug, Clone)] pub struct CachedRegion { - pub texture: Texture, - pub texture_size: UVec2, + pub output: Poll, + pub size: UVec2, pub tiles: Vec, - pub metadata: rendering::RenderMetadata, last_access: u64, memory_size: usize, } @@ -331,19 +329,19 @@ fn render_output_cache( #[scope(crate::platform_application_io::editor_api::IDENTIFIER)] editor_api: Item>, data: impl Node, Output = Item> + Send + Sync, #[data] tile_cache: TileCache, -) -> Result, Interrupt> { +) -> Poll> { let footprint = ctx.footprint(); let Some(render_params) = ctx.vararg(0).ok().and_then(|v| v.downcast_ref::()) else { log::warn!("render_output_cache: missing or invalid render params, falling back to direct render"); let context = OwnedContextImpl::from(ctx.clone()).with_footprint(*footprint); - return data.eval(context.into_context()); + return data.eval(&context.into_context()); }; // Fall back to direct render for non-Vello or zero-size viewports let physical_resolution = footprint.resolution; if !matches!(render_params.render_output_type, RenderOutputTypeRequest::Vello) || physical_resolution.x == 0 || physical_resolution.y == 0 { let context = OwnedContextImpl::from(ctx.clone()).with_footprint(*footprint).with_vararg(Box::new(render_params.clone())); - return data.eval(context.into_context()); + return data.eval(&context.into_context()); } let zoom = footprint.scale_magnitudes().x; @@ -375,34 +373,45 @@ fn render_output_cache( let cache_query = tile_cache.query(&viewport_bounds_device, &cache_key, max_region_area); + // If no regions, fall back to direct render + if cache_query.cached_regions.is_empty() && cache_query.missing_regions.iter().all(|region| region.tiles.is_empty()) { + let context = OwnedContextImpl::from(ctx.clone()).with_footprint(*footprint).with_vararg(Box::new(render_params.clone())); + return data.eval(&context.into_context()); + } + let mut new_regions = Vec::new(); - for missing_region in &cache_query.missing_regions { - if missing_region.tiles.is_empty() { - continue; + for missing_region in cache_query.missing_regions.iter().filter(|region| !region.tiles.is_empty()) { + let region = render_missing_region(missing_region, |ctx| data.eval(&ctx), ctx.clone(), render_params, &footprint.transform, &device_origin_offset); + match ®ion.output { + Poll::Pending => return Poll::Pending, + Poll::Error(error) => return Poll::Error(error.clone()), + Poll::Final(_) | Poll::Partial(_) | Poll::Fallback(_) => new_regions.push(region), } - let region = render_missing_region(missing_region, |ctx| data.eval(ctx), ctx.clone(), render_params, &footprint.transform, &device_origin_offset)?; - new_regions.push(region); } tile_cache.store_regions(new_regions.clone()); let all_regions: Vec<_> = cache_query.cached_regions.into_iter().chain(new_regions).collect(); - - // If no regions, fall back to direct render - if all_regions.is_empty() { - let context = OwnedContextImpl::from(ctx.clone()).with_footprint(*footprint).with_vararg(Box::new(render_params.clone())); - return data.eval(context.into_context()); - } + let partial = all_regions.iter().any(|region| matches!(region.output, Poll::Partial(_))); + let error = all_regions.iter().find_map(|region| match ®ion.output { + Poll::Fallback(boxed) => Some(boxed.1.clone()), + Poll::Final(_) | Poll::Partial(_) | Poll::Pending | Poll::Error(_) => None, + }); let executor = executor.into_element().expect("GPU executor not available"); let output_texture = executor.request_texture(physical_resolution); let combined_metadata = composite_cached_regions(&all_regions, &output_texture, &device_origin_offset, &footprint.transform, &executor); - Ok(Item::new_from_element(RenderOutput { + let output = Item::new_from_element(RenderOutput { data: RenderOutputType::Texture(output_texture), metadata: combined_metadata, - })) + }); + match (partial, error) { + (true, _) => Poll::Partial(output), + (false, Some(error)) => Poll::Fallback(Box::new((output, error))), + (false, None) => Poll::Final(output), + } } fn render_missing_region( @@ -412,9 +421,9 @@ fn render_missing_region( render_params: &RenderParams, viewport_transform: &DAffine2, viewport_origin_offset: &DVec2, -) -> Result +) -> CachedRegion where - F: Fn(Context<'static>) -> Result, Interrupt>, + F: Fn(Context<'static>) -> Poll>, { let min_tile = region.tiles.iter().fold(IVec2::new(i32::MAX, i32::MAX), |acc, t| acc.min(IVec2::new(t.x, t.y))); let max_tile = region.tiles.iter().fold(IVec2::new(i32::MIN, i32::MIN), |acc, t| acc.max(IVec2::new(t.x, t.y))); @@ -432,25 +441,19 @@ where let region_params = render_params.clone(); let region_ctx = OwnedContextImpl::from(ctx).with_footprint(region_footprint).with_vararg(Box::new(region_params)).into_context(); - let mut result = render_fn(region_ctx)?.into_element(); - - let RenderOutputType::Texture(texture) = result.data else { - unreachable!("render_missing_region: expected texture output from Vello render"); - }; - - let pixel_to_document = region_transform.inverse(); - result.metadata.apply_transform(pixel_to_document); - - let memory_size = (region_pixel_size.x * region_pixel_size.y) as usize * BYTES_PER_PIXEL; - - Ok(CachedRegion { - texture, - texture_size: region_pixel_size, + let output = render_fn(region_ctx).map(|output| { + let mut output = output.into_element(); + output.metadata.apply_transform(region_transform.inverse()); + output + }); + + CachedRegion { + output, + size: region_pixel_size, tiles: region.tiles.clone(), - metadata: result.metadata, last_access: 0, - memory_size, - }) + memory_size: (region_pixel_size.x * region_pixel_size.y) as usize * BYTES_PER_PIXEL, + } } fn composite_cached_regions( @@ -468,29 +471,38 @@ fn composite_cached_regions( let mut combined_metadata = rendering::RenderMetadata::default(); for region in regions { + let output = match ®ion.output { + Poll::Final(output) | Poll::Partial(output) => output, + Poll::Fallback(boxed) => &boxed.0, + Poll::Pending | Poll::Error(_) => unreachable!("Only rendered regions are composited"), + }; + let RenderOutputType::Texture(texture) = &output.data else { + unreachable!("composite_cached_regions: expected texture output from Vello render"); + }; + let min_tile = region.tiles.iter().fold(IVec2::new(i32::MAX, i32::MAX), |acc, t| acc.min(IVec2::new(t.x, t.y))); // Convert global tile position to physical pixel offset in the output texture let offset_pixels = min_tile * TILE_SIZE as i32 + device_origin_offset.round().as_ivec2(); let (src_x, dst_x, width) = if offset_pixels.x >= 0 { - (0, offset_pixels.x as u32, region.texture_size.x.min(output_resolution.x.saturating_sub(offset_pixels.x as u32))) + (0, offset_pixels.x as u32, region.size.x.min(output_resolution.x.saturating_sub(offset_pixels.x as u32))) } else { let skip = (-offset_pixels.x) as u32; - (skip, 0, region.texture_size.x.saturating_sub(skip).min(output_resolution.x)) + (skip, 0, region.size.x.saturating_sub(skip).min(output_resolution.x)) }; let (src_y, dst_y, height) = if offset_pixels.y >= 0 { - (0, offset_pixels.y as u32, region.texture_size.y.min(output_resolution.y.saturating_sub(offset_pixels.y as u32))) + (0, offset_pixels.y as u32, region.size.y.min(output_resolution.y.saturating_sub(offset_pixels.y as u32))) } else { let skip = (-offset_pixels.y) as u32; - (skip, 0, region.texture_size.y.saturating_sub(skip).min(output_resolution.y)) + (skip, 0, region.size.y.saturating_sub(skip).min(output_resolution.y)) }; if width > 0 && height > 0 { encoder.copy_texture_to_texture( wgpu::TexelCopyTextureInfo { - texture: region.texture.as_ref(), + texture: texture.as_ref(), mip_level: 0, origin: wgpu::Origin3d { x: src_x, y: src_y, z: 0 }, aspect: wgpu::TextureAspect::All, @@ -509,7 +521,7 @@ fn composite_cached_regions( ); } - let mut region_metadata = region.metadata.clone(); + let mut region_metadata = output.metadata.clone(); region_metadata.apply_transform(*viewport_transform); combined_metadata.merge(®ion_metadata); } From 826018d054d270a6c6bc89a6970a01ed0423ca56 Mon Sep 17 00:00:00 2001 From: Timon Date: Thu, 15 Oct 2026 17:12:56 +0000 Subject: [PATCH 43/54] Keep last frame while graph is pending --- editor/src/node_graph_executor.rs | 22 +++++++++++++++------- editor/src/node_graph_executor/runtime.rs | 4 ++-- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index 0e9f6c69222..940a094c8dd 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -44,6 +44,7 @@ pub struct ExecutionResponse { #[derive(Debug)] pub enum ExecutionResult { + Pending, Value(TaggedValue), Fallback(TaggedValue, EvaluationError), Error(EvaluationError), @@ -404,7 +405,8 @@ impl NodeGraphExecutor { responses.add(OverlaysMessage::Draw); - if execution_context.export_config.is_none() { + // A pending graph keeps the last frame, and the errors shown with it + if execution_context.export_config.is_none() && !matches!(result, ExecutionResult::Pending) { let errors = match &result { ExecutionResult::Fallback(_, errors) | ExecutionResult::Error(errors) => errors .iter() @@ -413,13 +415,14 @@ impl NodeGraphExecutor { message: error.to_string(), }) .collect(), - ExecutionResult::Value(_) | ExecutionResult::Panic(_) => Vec::new(), + ExecutionResult::Pending | ExecutionResult::Value(_) | ExecutionResult::Panic(_) => Vec::new(), }; responses.add(NodeGraphMessage::UpdateEvaluationErrors { errors }); } let node_graph_output = match result { - ExecutionResult::Value(value) | ExecutionResult::Fallback(value, _) => Ok(value), + ExecutionResult::Pending => Ok(None), + ExecutionResult::Value(value) | ExecutionResult::Fallback(value, _) => Ok(Some(value)), ExecutionResult::Error(errors) => Err(errors.iter().map(|(error, _)| error.to_string()).collect::>().join("\n")), ExecutionResult::Panic(payload) => { let message = payload.downcast_ref::<&str>().copied().or_else(|| payload.downcast_ref::().map(String::as_str)); @@ -440,11 +443,16 @@ impl NodeGraphExecutor { responses.extend(existing_responses.into_iter().map(Into::into)); document.network_interface.update_vector_modify(vector_modify); - if let Some(export_config) = execution_context.export_config { + match (node_graph_output, execution_context.export_config) { // Special handling for exporting the artwork - self.process_export(node_graph_output, export_config, document, responses)?; - } else { - self.process_node_graph_output(node_graph_output, responses)?; + (Some(node_graph_output), Some(export_config)) => self.process_export(node_graph_output, export_config, document, responses)?, + (Some(node_graph_output), None) => self.process_node_graph_output(node_graph_output, responses)?, + (None, Some(_)) => responses.add(DialogMessage::DisplayDialogError { + title: "Unable to export document".to_string(), + description: "Part of the document is still loading. Export again once it has finished.".to_string(), + }), + // The last frame stays until the async source completes and the graph renders again + (None, None) => {} } responses.add(DeferMessage::TriggerGraphRun { execution_id, diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index 1b24d28f723..72a936a5a98 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -219,7 +219,7 @@ impl NodeRuntime { } let mut result = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| self.execute_network(render_config))) { - Ok(Ok(Poll::Pending)) => continue, + Ok(Ok(Poll::Pending)) => ExecutionResult::Pending, Ok(Ok(Poll::Final(value) | Poll::Partial(value))) => ExecutionResult::Value(value), Ok(Ok(Poll::Fallback(boxed))) => { let (value, error) = *boxed; @@ -251,7 +251,7 @@ impl NodeRuntime { let output = match &mut result { ExecutionResult::Value(value) | ExecutionResult::Fallback(value, _) => Some(value), - ExecutionResult::Error(_) | ExecutionResult::Panic(_) => None, + ExecutionResult::Pending | ExecutionResult::Error(_) | ExecutionResult::Panic(_) => None, }; let texture = if let Some(TaggedValue::RenderOutput(RenderOutput { data, .. })) = output && let RenderOutputType::Texture(texture) = data From 396bfcc56af4b845fcef11c8722d357e5dee2d35 Mon Sep 17 00:00:00 2001 From: Timon Date: Thu, 15 Oct 2026 17:26:46 +0000 Subject: [PATCH 44/54] Treat partial export output as pending --- editor/src/node_graph_executor/runtime.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index 72a936a5a98..048df43a248 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -220,6 +220,7 @@ impl NodeRuntime { let mut result = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| self.execute_network(render_config))) { Ok(Ok(Poll::Pending)) => ExecutionResult::Pending, + Ok(Ok(Poll::Partial(_))) if render_config.for_export => ExecutionResult::Pending, Ok(Ok(Poll::Final(value) | Poll::Partial(value))) => ExecutionResult::Value(value), Ok(Ok(Poll::Fallback(boxed))) => { let (value, error) = *boxed; From 42926b8f6a52778a60ce27c6785fdd187624afe7 Mon Sep 17 00:00:00 2001 From: Timon Date: Fri, 16 Oct 2026 18:06:22 +0000 Subject: [PATCH 45/54] Move environment from global to dispatcher --- editor/src/application.rs | 46 +++++++------ editor/src/dispatcher.rs | 68 +++++++++++++------ .../app_window/app_window_message_handler.rs | 4 +- .../input_mapper_message_handler.rs | 12 ++-- .../messages/input_mapper/input_mappings.rs | 16 ++--- .../key_mapping_message_handler.rs | 20 ++++-- .../input_mapper/utility_types/keyboard.rs | 51 ++++---------- .../input_mapper/utility_types/macros.rs | 4 +- .../input_mapper/utility_types/misc.rs | 25 +++---- .../input_preprocessor_message_handler.rs | 33 +++++---- .../messages/layout/layout_message_handler.rs | 23 +++++-- .../layout/utility_types/layout_widget.rs | 52 ++++++++------ .../document/document_message_handler.rs | 5 +- .../navigation/navigation_message_handler.rs | 6 +- .../portfolio/portfolio_message_handler.rs | 26 ++++--- editor/src/messages/tool/utility_types.rs | 16 ++--- 16 files changed, 233 insertions(+), 174 deletions(-) diff --git a/editor/src/application.rs b/editor/src/application.rs index 2a46402fa32..3b1feb9cfaf 100644 --- a/editor/src/application.rs +++ b/editor/src/application.rs @@ -4,7 +4,7 @@ use crate::task::TaskExecutor; use graph_craft::application_io::PlatformApplicationIo; use graph_craft::application_io::resource::ResourceStorage; pub use graphene_std::uuid::*; -use std::sync::{Arc, OnceLock}; +use std::sync::Arc; pub struct Editor { pub dispatcher: Dispatcher, @@ -19,11 +19,10 @@ impl Editor { mut application_io: PlatformApplicationIo, wake: Wake, ) -> Self { - ENVIRONMENT.set(environment).expect("Editor shoud only be initialized once"); graphene_std::uuid::set_uuid_seed(uuid_random_seed); let task_executor = TaskExecutor::new(); - let mut dispatcher = Dispatcher::new(resource_storage, document_store); + let mut dispatcher = Dispatcher::new(environment, resource_storage, document_store); dispatcher.message_handlers.future_message_handler = FutureMessageHandler::new(task_executor.handle(), wake.clone()); application_io.inject_resource_proxy(dispatcher.message_handlers.resource_storage_message_handler.resources()); crate::node_graph_executor::replace_application_io(application_io); @@ -34,7 +33,6 @@ impl Editor { #[cfg(test)] pub(crate) fn new_local_executor() -> (Self, crate::node_graph_executor::NodeRuntime) { - let _ = ENVIRONMENT.set(*Editor::environment()); graphene_std::uuid::set_uuid_seed(0); let (mut runtime, executor) = crate::node_graph_executor::NodeGraphExecutor::new_with_local_runtime(); @@ -62,27 +60,16 @@ impl Editor { } } -static ENVIRONMENT: OnceLock = OnceLock::new(); -impl Editor { - #[cfg(not(test))] - pub fn environment() -> &'static Environment { - ENVIRONMENT.get().expect("Editor environment accessed before initialization") - } - - #[cfg(test)] - pub fn environment() -> &'static Environment { - &Environment { - platform: Platform::Desktop, - host: Host::Linux, - } - } -} - #[derive(Clone, Copy, Debug)] pub struct Environment { pub platform: Platform, pub host: Host, } +#[cfg(test)] +pub(crate) const DEFAULT_ENVIRONMENT: Environment = Environment { + platform: Platform::Desktop, + host: Host::Linux, +}; #[derive(Clone, Copy, Debug)] pub enum Platform { Desktop, @@ -112,6 +99,25 @@ impl Environment { } } +pub trait EnvironmentDisplay { + fn fmt(&self, f: &mut std::fmt::Formatter, environment: Environment) -> std::fmt::Result; + + fn display(&self, environment: Environment) -> WithEnvironment<'_, Self> { + WithEnvironment { value: self, environment } + } +} + +pub struct WithEnvironment<'a, T: ?Sized> { + value: &'a T, + environment: Environment, +} + +impl std::fmt::Display for WithEnvironment<'_, T> { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + self.value.fmt(f, self.environment) + } +} + pub const GRAPHITE_RELEASE_SERIES: &str = env!("GRAPHITE_RELEASE_SERIES"); pub const GRAPHITE_GIT_COMMIT_BRANCH: Option<&str> = option_env!("GRAPHITE_GIT_COMMIT_BRANCH"); pub const GRAPHITE_GIT_COMMIT_HASH: &str = env!("GRAPHITE_GIT_COMMIT_HASH"); diff --git a/editor/src/dispatcher.rs b/editor/src/dispatcher.rs index 9aa8bc31889..7507551b865 100644 --- a/editor/src/dispatcher.rs +++ b/editor/src/dispatcher.rs @@ -1,3 +1,4 @@ +use crate::application::Environment; use crate::messages::debug::utility_types::MessageLoggingVerbosity; use crate::messages::defer::DeferMessageContext; use crate::messages::dialog::DialogMessageContext; @@ -9,15 +10,16 @@ use crate::messages::tool::common_functionality::utility_functions::make_path_ed use graph_craft::application_io::resource::ResourceStorage; use std::sync::Arc; -#[derive(Debug, Default)] +#[derive(Debug)] pub struct Dispatcher { + environment: Environment, message_queues: Vec>, pub responses: Vec, pub frontend_update_messages: Vec, pub message_handlers: DispatcherMessageHandlers, } -#[derive(Debug, Default)] +#[derive(Debug)] pub struct DispatcherMessageHandlers { animation_message_handler: AnimationMessageHandler, app_window_message_handler: AppWindowMessageHandler, @@ -41,17 +43,27 @@ pub struct DispatcherMessageHandlers { } impl DispatcherMessageHandlers { - pub fn with_resource_storage(resource_storage: Arc) -> Self { + fn new(environment: Environment) -> Self { Self { - resource_storage_message_handler: ResourceStorageMessageHandler::new(resource_storage), - ..Self::default() - } - } - - pub fn with_executor(executor: crate::node_graph_executor::NodeGraphExecutor) -> Self { - Self { - portfolio_message_handler: PortfolioMessageHandler::with_executor(executor), - ..Self::default() + animation_message_handler: Default::default(), + app_window_message_handler: Default::default(), + future_message_handler: Default::default(), + broadcast_message_handler: Default::default(), + clipboard_message_handler: Default::default(), + color_picker_message_handler: Default::default(), + debug_message_handler: Default::default(), + defer_message_handler: Default::default(), + dialog_message_handler: Default::default(), + input_preprocessor_message_handler: Default::default(), + key_mapping_message_handler: KeyMappingMessageHandler::new(environment), + layout_message_handler: Default::default(), + menu_bar_message_handler: Default::default(), + network_message_handler: Default::default(), + portfolio_message_handler: Default::default(), + preferences_message_handler: Default::default(), + resource_storage_message_handler: Default::default(), + tool_message_handler: Default::default(), + viewport_message_handler: Default::default(), } } } @@ -89,8 +101,14 @@ const DEBUG_MESSAGE_BLOCK_LIST: &[MessageDiscriminant] = &[ const DEBUG_MESSAGE_ENDING_BLOCK_LIST: &[&str] = &["PointerMove", "PointerOutsideViewport", "Overlays", "Draw", "CurrentTime", "Time"]; impl Dispatcher { - pub fn new(resource_storage: Arc, document_store: Arc) -> Self { - let mut s = Self::default(); + pub fn new(environment: Environment, resource_storage: Arc, document_store: Arc) -> Self { + let mut s = Self { + environment, + message_queues: Vec::new(), + responses: Vec::new(), + frontend_update_messages: Vec::new(), + message_handlers: DispatcherMessageHandlers::new(environment), + }; s.message_handlers.resource_storage_message_handler = ResourceStorageMessageHandler::new(resource_storage); s.message_handlers.portfolio_message_handler.set_document_store(document_store); s @@ -99,6 +117,7 @@ impl Dispatcher { #[cfg(test)] pub fn with_executor(executor: crate::node_graph_executor::NodeGraphExecutor) -> Self { let mut s = Self::new( + crate::application::DEFAULT_ENVIRONMENT, Arc::new(graph_craft::application_io::resource::HashMapResourceStorage::new()), Arc::new(document_container::store::MemoryStore::default()), ); @@ -224,6 +243,7 @@ impl Dispatcher { message, &mut queue, InputPreprocessorMessageContext { + environment: self.environment, viewport: &self.message_handlers.viewport_message_handler, }, ); @@ -232,13 +252,22 @@ impl Dispatcher { let input = &self.message_handlers.input_preprocessor_message_handler; let actions = self.collect_actions(); - self.message_handlers - .key_mapping_message_handler - .process_message(message, &mut queue, KeyMappingMessageContext { input, actions }); + self.message_handlers.key_mapping_message_handler.process_message( + message, + &mut queue, + KeyMappingMessageContext { + environment: self.environment, + input, + actions, + }, + ); } Message::Layout(message) => { - let action_input_mapping = &|action_to_find: &MessageDiscriminant| self.message_handlers.key_mapping_message_handler.action_input_mapping(action_to_find); - let context = LayoutMessageContext { action_input_mapping }; + let action_input_mapping = &|action_to_find: &MessageDiscriminant| self.message_handlers.key_mapping_message_handler.action_input_mapping(action_to_find, self.environment); + let context = LayoutMessageContext { + environment: self.environment, + action_input_mapping, + }; self.message_handlers.layout_message_handler.process_message(message, &mut queue, context); } @@ -255,6 +284,7 @@ impl Dispatcher { message, &mut queue, PortfolioMessageContext { + environment: self.environment, ipp: &self.message_handlers.input_preprocessor_message_handler, preferences: &self.message_handlers.preferences_message_handler, current_tool: &self.message_handlers.tool_message_handler.tool_state.tool_data.active_tool_type, diff --git a/editor/src/messages/app_window/app_window_message_handler.rs b/editor/src/messages/app_window/app_window_message_handler.rs index e09ad3c19c1..dee5f8ea692 100644 --- a/editor/src/messages/app_window/app_window_message_handler.rs +++ b/editor/src/messages/app_window/app_window_message_handler.rs @@ -86,8 +86,8 @@ pub enum AppWindowPlatform { Linux, } -impl From<&Environment> for AppWindowPlatform { - fn from(environment: &Environment) -> Self { +impl From for AppWindowPlatform { + fn from(environment: Environment) -> Self { match (environment.platform, environment.host) { (Platform::Web, _) => AppWindowPlatform::Web, (Platform::Desktop, Host::Linux) => AppWindowPlatform::Linux, diff --git a/editor/src/messages/input_mapper/input_mapper_message_handler.rs b/editor/src/messages/input_mapper/input_mapper_message_handler.rs index 4797384142c..deac98f2cab 100644 --- a/editor/src/messages/input_mapper/input_mapper_message_handler.rs +++ b/editor/src/messages/input_mapper/input_mapper_message_handler.rs @@ -1,6 +1,6 @@ use super::utility_types::keyboard::KeysGroup; use super::utility_types::misc::Mapping; -use crate::application::Editor; +use crate::application::Environment; use crate::messages::input_mapper::utility_types::keyboard::{self, Key}; use crate::messages::input_mapper::utility_types::misc::MappingEntry; use crate::messages::prelude::*; @@ -11,7 +11,7 @@ pub struct InputMapperMessageContext<'a> { pub actions: ActionList, } -#[derive(Debug, Default, ExtractField)] +#[derive(Debug, ExtractField)] pub struct InputMapperMessageHandler { mapping: Mapping, } @@ -29,11 +29,15 @@ impl MessageHandler> for Input } impl InputMapperMessageHandler { + pub fn new(mapping: Mapping) -> Self { + Self { mapping } + } + pub fn set_mapping(&mut self, mapping: Mapping) { self.mapping = mapping; } - pub fn action_input_mapping(&self, action_to_find: &MessageDiscriminant) -> Option { + pub fn action_input_mapping(&self, action_to_find: &MessageDiscriminant, environment: Environment) -> Option { let all_key_mapping_entries = std::iter::empty() .chain(self.mapping.key_up.iter()) .chain(self.mapping.key_down.iter()) @@ -48,7 +52,7 @@ impl InputMapperMessageHandler { let found_actions = all_mapping_entries.filter(|entry| entry.action.to_discriminant() == *action_to_find); // Get the `Key` for this platform's accelerator key - let platform_accel_key = if Editor::environment().is_mac() { Key::Command } else { Key::Control }; + let platform_accel_key = if environment.is_mac() { Key::Command } else { Key::Control }; let entry_to_key = |entry: &MappingEntry| { // Get the modifier keys for the entry (and convert them to Key) diff --git a/editor/src/messages/input_mapper/input_mappings.rs b/editor/src/messages/input_mapper/input_mappings.rs index fcd6bd4563a..4ffaba9b7a5 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -1,4 +1,4 @@ -use crate::application::Editor; +use crate::application::Environment; use crate::consts::{BIG_NUDGE_AMOUNT, BRUSH_SIZE_CHANGE_KEYBOARD, NUDGE_AMOUNT}; use crate::messages::input_mapper::key_mapping::MappingVariant; use crate::messages::input_mapper::utility_types::keyboard::{Key, KeyStates}; @@ -13,20 +13,20 @@ use crate::messages::tool::tool_messages::brush_tool::BrushToolMessageOptionsUpd use crate::messages::tool::tool_messages::select_tool::SelectToolPointerKeys; use glam::DVec2; -impl From for Mapping { - fn from(value: MappingVariant) -> Self { - match value { - MappingVariant::Default => input_mappings(false), - MappingVariant::ZoomWithScroll => input_mappings(true), +impl Mapping { + pub fn new(variant: MappingVariant, environment: Environment) -> Self { + match variant { + MappingVariant::Default => input_mappings(false, environment), + MappingVariant::ZoomWithScroll => input_mappings(true, environment), } } } -pub fn input_mappings(zoom_with_scroll: bool) -> Mapping { +pub fn input_mappings(zoom_with_scroll: bool, environment: Environment) -> Mapping { use InputMapperMessage::*; use Key::*; - let is_mac = Editor::environment().is_mac(); + let is_mac = environment.is_mac(); // NOTICE: // If a new mapping you added here isn't working (and perhaps another lower-precedence one is instead), make sure to advertise diff --git a/editor/src/messages/input_mapper/key_mapping/key_mapping_message_handler.rs b/editor/src/messages/input_mapper/key_mapping/key_mapping_message_handler.rs index 5f70b8df7d4..02511b10611 100644 --- a/editor/src/messages/input_mapper/key_mapping/key_mapping_message_handler.rs +++ b/editor/src/messages/input_mapper/key_mapping/key_mapping_message_handler.rs @@ -1,14 +1,18 @@ +use crate::application::Environment; use crate::messages::input_mapper::input_mapper_message_handler::InputMapperMessageContext; +use crate::messages::input_mapper::key_mapping::MappingVariant; use crate::messages::input_mapper::utility_types::keyboard::KeysGroup; +use crate::messages::input_mapper::utility_types::misc::Mapping; use crate::messages::prelude::*; #[derive(ExtractField)] pub struct KeyMappingMessageContext<'a> { + pub environment: Environment, pub input: &'a InputPreprocessorMessageHandler, pub actions: ActionList, } -#[derive(Debug, Default, ExtractField)] +#[derive(Debug, ExtractField)] pub struct KeyMappingMessageHandler { mapping_handler: InputMapperMessageHandler, } @@ -16,21 +20,27 @@ pub struct KeyMappingMessageHandler { #[message_handler_data] impl MessageHandler> for KeyMappingMessageHandler { fn process_message(&mut self, message: KeyMappingMessage, responses: &mut VecDeque, context: KeyMappingMessageContext) { - let KeyMappingMessageContext { input, actions } = context; + let KeyMappingMessageContext { environment, input, actions } = context; match message { // Sub-messages KeyMappingMessage::Lookup(input_message) => self.mapping_handler.process_message(input_message, responses, InputMapperMessageContext { input, actions }), // Messages - KeyMappingMessage::ModifyMapping { mapping } => self.mapping_handler.set_mapping(mapping.into()), + KeyMappingMessage::ModifyMapping { mapping } => self.mapping_handler.set_mapping(Mapping::new(mapping, environment)), } } advertise_actions!(); } impl KeyMappingMessageHandler { - pub fn action_input_mapping(&self, action_to_find: &MessageDiscriminant) -> Option { - self.mapping_handler.action_input_mapping(action_to_find) + pub fn new(environment: Environment) -> Self { + Self { + mapping_handler: InputMapperMessageHandler::new(Mapping::new(MappingVariant::Default, environment)), + } + } + + pub fn action_input_mapping(&self, action_to_find: &MessageDiscriminant, environment: Environment) -> Option { + self.mapping_handler.action_input_mapping(action_to_find, environment) } } diff --git a/editor/src/messages/input_mapper/utility_types/keyboard.rs b/editor/src/messages/input_mapper/utility_types/keyboard.rs index 041edb4ca03..1f9e66c2a56 100644 --- a/editor/src/messages/input_mapper/utility_types/keyboard.rs +++ b/editor/src/messages/input_mapper/utility_types/keyboard.rs @@ -1,4 +1,4 @@ -use crate::application::Editor; +use crate::application::{Environment, EnvironmentDisplay}; use crate::messages::prelude::*; use bitflags::bitflags; use std::fmt::{self, Display, Formatter}; @@ -243,9 +243,9 @@ pub enum Key { _KeysVariantCount, // This has to be the last element in the enum } -impl fmt::Display for Key { +impl EnvironmentDisplay for Key { // TODO: Relevant key labels should be localized when we get around to implementing localization/internationalization - fn fmt(&self, f: &mut fmt::Formatter) -> std::fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter, environment: Environment) -> std::fmt::Result { let key_name = format!("{self:?}"); // Writing system keys @@ -258,7 +258,7 @@ impl fmt::Display for Key { return write!(f, "{}", key_name.chars().skip(KEY_PREFIX.len()).collect::()); } - let is_mac = Editor::environment().is_mac(); + let is_mac = environment.is_mac(); let name = match self { // Writing system keys @@ -345,36 +345,10 @@ pub const NUMBER_OF_KEYS: usize = Key::_KeysVariantCount as usize - 1; // ========= /// Only `Key`s that exist on a physical keyboard should be used. +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] #[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct KeysGroup(pub Vec); -impl fmt::Display for KeysGroup { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - const JOINER_MARK: &str = " "; - - let mut joined = self - .0 - .iter() - .map(|key| { - let key_is_modifier = matches!(*key, Key::Control | Key::Command | Key::Alt | Key::Shift | Key::Meta | Key::Accel); - - if Editor::environment().is_mac() && key_is_modifier { - key.to_string() - } else { - key.to_string() + JOINER_MARK - } - }) - .collect::(); - - // Cut the joining character off the end, if present - if joined.ends_with(JOINER_MARK) { - joined.truncate(joined.len() - JOINER_MARK.len()); - } - - write!(f, "{joined}") - } -} - // ========== // LabeledKey // ========== @@ -425,13 +399,16 @@ pub enum LabeledKeyOrMouseMotion { MouseMotion(MouseMotion), } -impl From for LabeledKeyOrMouseMotion { - fn from(key: Key) -> Self { +impl LabeledKeyOrMouseMotion { + pub fn new(key: Key, environment: Environment) -> Self { match key { Key::MouseLeft => Self::MouseMotion(MouseMotion::Lmb), Key::MouseRight => Self::MouseMotion(MouseMotion::Rmb), Key::MouseMiddle => Self::MouseMotion(MouseMotion::Mmb), - _ => Self::Key(LabeledKey { key, label: key.to_string() }), + _ => Self::Key(LabeledKey { + key, + label: key.display(environment).to_string(), + }), } } } @@ -444,9 +421,9 @@ impl From for LabeledKeyOrMouseMotion { #[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct LabeledShortcut(pub Vec); -impl From for LabeledShortcut { - fn from(keys_group: KeysGroup) -> Self { - Self(keys_group.0.into_iter().map(|key| key.into()).collect()) +impl LabeledShortcut { + pub fn new(keys_group: KeysGroup, environment: Environment) -> Self { + Self(keys_group.0.into_iter().map(|key| LabeledKeyOrMouseMotion::new(key, environment)).collect()) } } diff --git a/editor/src/messages/input_mapper/utility_types/macros.rs b/editor/src/messages/input_mapper/utility_types/macros.rs index 2adda8b5409..fd9e22c065d 100644 --- a/editor/src/messages/input_mapper/utility_types/macros.rs +++ b/editor/src/messages/input_mapper/utility_types/macros.rs @@ -161,8 +161,8 @@ macro_rules! action_shortcut { macro_rules! action_shortcut_manual { ($($keys:expr),*) => { - Some(crate::messages::input_mapper::utility_types::misc::ActionShortcut::Shortcut( - crate::messages::input_mapper::utility_types::keyboard::LabeledShortcut(vec![$($keys.into()),*]).into(), + Some(crate::messages::input_mapper::utility_types::misc::ActionShortcut::Keys( + crate::messages::input_mapper::utility_types::keyboard::KeysGroup(vec![$($keys),*]), )) }; } diff --git a/editor/src/messages/input_mapper/utility_types/misc.rs b/editor/src/messages/input_mapper/utility_types/misc.rs index e06ed575742..4fb0c47b157 100644 --- a/editor/src/messages/input_mapper/utility_types/misc.rs +++ b/editor/src/messages/input_mapper/utility_types/misc.rs @@ -1,5 +1,5 @@ use super::keyboard::{KeysGroup, LabeledShortcut, all_required_modifiers_pressed}; -use crate::messages::input_mapper::key_mapping::MappingVariant; +use crate::application::Environment; use crate::messages::input_mapper::utility_types::keyboard::{KeyStates, NUMBER_OF_KEYS}; use crate::messages::input_mapper::utility_types::pointer::NUMBER_OF_MOUSE_BUTTONS; use crate::messages::prelude::*; @@ -17,12 +17,6 @@ pub struct Mapping { pub pointer_shake: KeyMappingEntries, } -impl Default for Mapping { - fn default() -> Self { - MappingVariant::Default.into() - } -} - impl Mapping { pub fn match_input_message(&self, message: InputMapperMessage, keyboard_state: &KeyStates, actions: ActionList) -> Option { let list = self.associated_entries(&message); @@ -111,19 +105,20 @@ pub struct MappingEntry { pub enum ActionShortcut { #[serde(skip)] Action(MessageDiscriminant), + #[serde(skip)] + Keys(KeysGroup), #[serde(rename = "shortcut")] Shortcut(LabeledShortcut), } impl ActionShortcut { - pub fn realize_shortcut(&mut self, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Option) { - if let Self::Action(action) = self { - if let Some(keys) = action_input_mapping(action) { - *self = Self::Shortcut(keys.into()); - } else { - *self = Self::Shortcut(KeysGroup::default().into()); - } - } + pub fn realize_shortcut(&mut self, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Option, environment: Environment) { + let keys = match self { + Self::Action(action) => action_input_mapping(action).unwrap_or_default(), + Self::Keys(keys) => std::mem::take(keys), + Self::Shortcut(_) => return, + }; + *self = Self::Shortcut(LabeledShortcut::new(keys, environment)); } } diff --git a/editor/src/messages/input_preprocessor/input_preprocessor_message_handler.rs b/editor/src/messages/input_preprocessor/input_preprocessor_message_handler.rs index 8e29e813f96..3a5751e2148 100644 --- a/editor/src/messages/input_preprocessor/input_preprocessor_message_handler.rs +++ b/editor/src/messages/input_preprocessor/input_preprocessor_message_handler.rs @@ -1,4 +1,4 @@ -use crate::application::Editor; +use crate::application::Environment; use crate::messages::input_mapper::utility_types::keyboard::{Key, KeyStates, ModifierKeys}; use crate::messages::input_mapper::utility_types::misc::FrameTimeInfo; use crate::messages::input_mapper::utility_types::pointer::{MouseButton, MouseKeys, PointerState}; @@ -7,6 +7,7 @@ use std::time::Duration; #[derive(ExtractField)] pub struct InputPreprocessorMessageContext<'a> { + pub environment: Environment, pub viewport: &'a ViewportMessageHandler, } @@ -22,11 +23,11 @@ pub struct InputPreprocessorMessageHandler { #[message_handler_data] impl<'a> MessageHandler> for InputPreprocessorMessageHandler { fn process_message(&mut self, message: InputPreprocessorMessage, responses: &mut VecDeque, context: InputPreprocessorMessageContext<'a>) { - let InputPreprocessorMessageContext { viewport } = context; + let InputPreprocessorMessageContext { environment, viewport } = context; match message { InputPreprocessorMessage::DoubleClick { editor_mouse_state, modifier_keys } => { - self.update_states_of_modifier_keys(modifier_keys, responses); + self.update_states_of_modifier_keys(modifier_keys, environment, responses); let pointer_state = editor_mouse_state.to_pointer_state(viewport); self.mouse.position = pointer_state.position; @@ -43,7 +44,7 @@ impl<'a> MessageHandler { - self.update_states_of_modifier_keys(modifier_keys, responses); + self.update_states_of_modifier_keys(modifier_keys, environment, responses); self.keyboard.set(key as usize); if !key_repeat { responses.add(InputMapperMessage::KeyDownNoRepeat(key)); @@ -51,7 +52,7 @@ impl<'a> MessageHandler { - self.update_states_of_modifier_keys(modifier_keys, responses); + self.update_states_of_modifier_keys(modifier_keys, environment, responses); self.keyboard.unset(key as usize); if !key_repeat { responses.add(InputMapperMessage::KeyUpNoRepeat(key)); @@ -59,7 +60,7 @@ impl<'a> MessageHandler { - self.update_states_of_modifier_keys(modifier_keys, responses); + self.update_states_of_modifier_keys(modifier_keys, environment, responses); let pointer_state = editor_mouse_state.to_pointer_state(viewport); self.mouse.position = pointer_state.position; @@ -67,7 +68,7 @@ impl<'a> MessageHandler { - self.update_states_of_modifier_keys(modifier_keys, responses); + self.update_states_of_modifier_keys(modifier_keys, environment, responses); let pointer_state = editor_mouse_state.to_pointer_state(viewport); self.mouse.position = pointer_state.position; @@ -78,7 +79,7 @@ impl<'a> MessageHandler { - self.update_states_of_modifier_keys(modifier_keys, responses); + self.update_states_of_modifier_keys(modifier_keys, environment, responses); let pointer_state = editor_mouse_state.to_pointer_state(viewport); self.mouse.position = pointer_state.position; @@ -86,7 +87,7 @@ impl<'a> MessageHandler { - self.update_states_of_modifier_keys(modifier_keys, responses); + self.update_states_of_modifier_keys(modifier_keys, environment, responses); let pointer_state = editor_mouse_state.to_pointer_state(viewport); self.mouse.position = pointer_state.position; @@ -99,7 +100,7 @@ impl<'a> MessageHandler { - self.update_states_of_modifier_keys(modifier_keys, responses); + self.update_states_of_modifier_keys(modifier_keys, environment, responses); let pointer_state = editor_mouse_state.to_pointer_state(viewport); self.mouse.position = pointer_state.position; @@ -155,7 +156,7 @@ impl InputPreprocessorMessageHandler { self.mouse = new_state; } - fn update_states_of_modifier_keys(&mut self, pressed_modifier_keys: ModifierKeys, responses: &mut VecDeque) { + fn update_states_of_modifier_keys(&mut self, pressed_modifier_keys: ModifierKeys, environment: Environment, responses: &mut VecDeque) { let is_key_pressed = |key_to_check: ModifierKeys| pressed_modifier_keys.contains(key_to_check); // Update the state of the concrete modifier keys based on the source state @@ -164,14 +165,14 @@ impl InputPreprocessorMessageHandler { self.update_modifier_key(Key::Control, is_key_pressed(ModifierKeys::CONTROL), responses); // Update the state of either the concrete Meta or the Command keys based on which one is applicable for this platform - let meta_or_command = match Editor::environment().is_mac() { + let meta_or_command = match environment.is_mac() { true => Key::Command, false => Key::Meta, }; self.update_modifier_key(meta_or_command, is_key_pressed(ModifierKeys::META_OR_COMMAND), responses); // Update the state of the virtual Accel key (the primary accelerator key) based on the source state of the Control or Command key, whichever is relevant on this platform - let accel_virtual_key_state = match Editor::environment().is_mac() { + let accel_virtual_key_state = match environment.is_mac() { true => is_key_pressed(ModifierKeys::META_OR_COMMAND), false => is_key_pressed(ModifierKeys::CONTROL), }; @@ -193,6 +194,7 @@ impl InputPreprocessorMessageHandler { #[cfg(test)] mod test { + use crate::application::DEFAULT_ENVIRONMENT; use crate::messages::input_mapper::utility_types::keyboard::{Key, ModifierKeys}; use crate::messages::input_mapper::utility_types::pointer::EditorPointerState; use crate::messages::prelude::*; @@ -211,6 +213,7 @@ mod test { let mut responses = VecDeque::new(); let context = InputPreprocessorMessageContext { + environment: DEFAULT_ENVIRONMENT, viewport: &ViewportMessageHandler::default(), }; input_preprocessor.process_message(message, &mut responses, context); @@ -230,6 +233,7 @@ mod test { let mut responses = VecDeque::new(); let context = InputPreprocessorMessageContext { + environment: DEFAULT_ENVIRONMENT, viewport: &ViewportMessageHandler::default(), }; input_preprocessor.process_message(message, &mut responses, context); @@ -249,6 +253,7 @@ mod test { let mut responses = VecDeque::new(); let context = InputPreprocessorMessageContext { + environment: DEFAULT_ENVIRONMENT, viewport: &ViewportMessageHandler::default(), }; input_preprocessor.process_message(message, &mut responses, context); @@ -270,6 +275,7 @@ mod test { let mut responses = VecDeque::new(); let context = InputPreprocessorMessageContext { + environment: DEFAULT_ENVIRONMENT, viewport: &ViewportMessageHandler::default(), }; input_preprocessor.process_message(message, &mut responses, context); @@ -290,6 +296,7 @@ mod test { let mut responses = VecDeque::new(); let context = InputPreprocessorMessageContext { + environment: DEFAULT_ENVIRONMENT, viewport: &ViewportMessageHandler::default(), }; input_preprocessor.process_message(message, &mut responses, context); diff --git a/editor/src/messages/layout/layout_message_handler.rs b/editor/src/messages/layout/layout_message_handler.rs index 7c8cb241e0c..37d83481a18 100644 --- a/editor/src/messages/layout/layout_message_handler.rs +++ b/editor/src/messages/layout/layout_message_handler.rs @@ -1,3 +1,4 @@ +use crate::application::Environment; use crate::messages::input_mapper::utility_types::keyboard::KeysGroup; use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier; @@ -9,6 +10,7 @@ use std::collections::HashMap; #[derive(ExtractField)] pub struct LayoutMessageContext<'a> { + pub environment: Environment, pub action_input_mapping: &'a dyn Fn(&MessageDiscriminant) -> Option, } @@ -20,6 +22,7 @@ pub struct LayoutMessageHandler { #[message_handler_data] impl MessageHandler> for LayoutMessageHandler { fn process_message(&mut self, message: LayoutMessage, responses: &mut std::collections::VecDeque, context: LayoutMessageContext) { + let environment = context.environment; let action_input_mapping = &context.action_input_mapping; match message { @@ -33,7 +36,7 @@ impl MessageHandler> for LayoutMessageHa return; }; // Resend that diff - self.send_diff(vec![diff], layout_target, responses, action_input_mapping); + self.send_diff(vec![diff], layout_target, responses, action_input_mapping, environment); } LayoutMessage::ResendAllLayouts => { // Collect non-empty layouts and their indices, then clear the stored copies so diffs compute as full re-sends @@ -46,11 +49,11 @@ impl MessageHandler> for LayoutMessageHa .collect(); for (layout_target, layout) in layouts_to_resend { - self.diff_and_send_layout_to_frontend(layout_target, layout, responses, action_input_mapping); + self.diff_and_send_layout_to_frontend(layout_target, layout, responses, action_input_mapping, environment); } } LayoutMessage::SendLayout { layout, layout_target } => { - self.diff_and_send_layout_to_frontend(layout_target, layout, responses, action_input_mapping); + self.diff_and_send_layout_to_frontend(layout_target, layout, responses, action_input_mapping, environment); } LayoutMessage::DestroyLayout { layout_target } => { if let Some(layout) = self.layouts.get_mut(layout_target as usize) { @@ -506,6 +509,7 @@ impl LayoutMessageHandler { mut new_layout: Layout, responses: &mut VecDeque, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Option, + environment: Environment, ) { // Collect CheckboxId mappings from new layout let mut checkbox_map = HashMap::new(); @@ -536,12 +540,19 @@ impl LayoutMessageHandler { }]; } - self.send_diff(widget_diffs, layout_target, responses, action_input_mapping); + self.send_diff(widget_diffs, layout_target, responses, action_input_mapping, environment); } /// Send a diff to the frontend based on the layout target. - fn send_diff(&self, mut diff: Vec, layout_target: LayoutTarget, responses: &mut VecDeque, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Option) { - diff.iter_mut().for_each(|diff| diff.new_value.apply_keyboard_shortcut(action_input_mapping)); + fn send_diff( + &self, + mut diff: Vec, + layout_target: LayoutTarget, + responses: &mut VecDeque, + action_input_mapping: &impl Fn(&MessageDiscriminant) -> Option, + environment: Environment, + ) { + diff.iter_mut().for_each(|diff| diff.new_value.apply_keyboard_shortcut(action_input_mapping, environment)); if matches!(layout_target, LayoutTarget::_LayoutTargetLength) { panic!("`_LayoutTargetLength` is not a valid `LayoutTarget` and is used for array indexing"); diff --git a/editor/src/messages/layout/utility_types/layout_widget.rs b/editor/src/messages/layout/utility_types/layout_widget.rs index 114b6a55f89..2336b00cc27 100644 --- a/editor/src/messages/layout/utility_types/layout_widget.rs +++ b/editor/src/messages/layout/utility_types/layout_widget.rs @@ -1,7 +1,7 @@ use super::widgets::button_widgets::*; use super::widgets::input_widgets::*; use super::widgets::label_widgets::*; -use crate::application::generate_uuid; +use crate::application::{Environment, generate_uuid}; use crate::messages::input_mapper::utility_types::keyboard::KeysGroup; use crate::messages::prelude::*; use std::collections::HashMap; @@ -860,7 +860,7 @@ pub enum DiffUpdate { impl DiffUpdate { /// Append the keyboard shortcut to the tooltip where applicable - pub fn apply_keyboard_shortcut(&mut self, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Option) { + pub fn apply_keyboard_shortcut(&mut self, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Option, environment: Environment) { // Go through each widget to convert `ActionShortcut::Action` to `ActionShortcut::Shortcut` and append the key combination to the widget tooltip let convert_tooltip = |widget_instance: &mut WidgetInstance| { // Handle all the widgets that have tooltips @@ -895,7 +895,7 @@ impl DiffUpdate { // Convert `ActionShortcut::Action` to `ActionShortcut::Shortcut` if let Some(tooltip_shortcut) = tooltip_shortcut { - tooltip_shortcut.realize_shortcut(action_input_mapping); + tooltip_shortcut.realize_shortcut(action_input_mapping, environment); } // Handle RadioInput separately because its tooltips are children of the widget @@ -903,23 +903,23 @@ impl DiffUpdate { for radio_entry_data in &mut radio_input.entries { // Convert `ActionShortcut::Action` to `ActionShortcut::Shortcut` if let Some(tooltip_shortcut) = radio_entry_data.tooltip_shortcut.as_mut() { - tooltip_shortcut.realize_shortcut(action_input_mapping); + tooltip_shortcut.realize_shortcut(action_input_mapping, environment); } } } }; // Recursively fill menu list entries with their realized shortcut keys specific to the current bindings and platform - fn apply_action_shortcut_to_menu_lists(entry_sections: &mut MenuListEntrySections, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Option) { + fn apply_action_shortcut_to_menu_lists(entry_sections: &mut MenuListEntrySections, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Option, environment: Environment) { for entries in entry_sections { for entry in entries { // Convert `ActionShortcut::Action` to `ActionShortcut::Shortcut` if let Some(tooltip_shortcut) = &mut entry.tooltip_shortcut { - tooltip_shortcut.realize_shortcut(action_input_mapping); + tooltip_shortcut.realize_shortcut(action_input_mapping, environment); } // Recursively call this function on the menu's children - apply_action_shortcut_to_menu_lists(&mut entry.children, action_input_mapping); + apply_action_shortcut_to_menu_lists(&mut entry.children, action_input_mapping, environment); } } } @@ -949,29 +949,39 @@ impl DiffUpdate { // Apply shortcut conversions to all widgets that have menu lists let convert_menu_lists = |widget_instance: &mut WidgetInstance| match &mut *widget_instance.widget { Widget::DropdownInput(dropdown_input) => { - apply_action_shortcut_to_menu_lists(&mut dropdown_input.entries, action_input_mapping); + apply_action_shortcut_to_menu_lists(&mut dropdown_input.entries, action_input_mapping, environment); dropdown_input.entries_hash = hash_menu_list_entry_sections(&dropdown_input.entries); } Widget::TextButton(text_button) => { - apply_action_shortcut_to_menu_lists(&mut text_button.menu_list_children, action_input_mapping); + apply_action_shortcut_to_menu_lists(&mut text_button.menu_list_children, action_input_mapping, environment); text_button.menu_list_children_hash = hash_menu_list_entry_sections(&text_button.menu_list_children); } _ => {} }; - match self { - Self::Layout(layout) => layout.0.iter_mut().flat_map(|layout_group| layout_group.iter_mut()).for_each(|widget_instance| { - convert_tooltip(widget_instance); - convert_menu_lists(widget_instance); - }), - Self::LayoutGroup(layout_group) => layout_group.iter_mut().for_each(|widget_instance| { - convert_tooltip(widget_instance); - convert_menu_lists(widget_instance); - }), - Self::Widget(widget_instance) => { - convert_tooltip(widget_instance); - convert_menu_lists(widget_instance); + let convert = |widget_instance: &mut WidgetInstance| { + convert_tooltip(widget_instance); + convert_menu_lists(widget_instance); + }; + + fn convert_layout_group(layout_group: &mut LayoutGroup, convert: &impl Fn(&mut WidgetInstance)) { + match layout_group { + LayoutGroup::Column(WidgetColumn { widgets }) | LayoutGroup::Row(WidgetRow { widgets }) => widgets.iter_mut().for_each(|widget_instance| convert_widget(widget_instance, convert)), + LayoutGroup::Table(WidgetTable { rows, .. }) => rows.iter_mut().flatten().flatten().for_each(|widget_instance| convert_widget(widget_instance, convert)), + LayoutGroup::Section(WidgetSection { layout, .. }) => layout.0.iter_mut().for_each(|layout_group| convert_layout_group(layout_group, convert)), + } + } + fn convert_widget(widget_instance: &mut WidgetInstance, convert: &impl Fn(&mut WidgetInstance)) { + convert(widget_instance); + if let Widget::PopoverButton(popover_button) = &mut *widget_instance.widget { + popover_button.popover_layout.0.iter_mut().for_each(|layout_group| convert_layout_group(layout_group, convert)); } } + + match self { + Self::Layout(layout) => layout.0.iter_mut().for_each(|layout_group| convert_layout_group(layout_group, &convert)), + Self::LayoutGroup(layout_group) => convert_layout_group(layout_group, &convert), + Self::Widget(widget_instance) => convert_widget(widget_instance, &convert), + } } } diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 3b5405f0a58..a0e6600b50a 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -5,7 +5,7 @@ use super::utility_types::misc::{GroupFolderType, SNAP_FUNCTIONS_FOR_BOUNDING_BO use super::utility_types::network_interface::{self, NodeNetworkInterface, TransactionStatus}; use super::utility_types::nodes::{CollapsedLayers, LayerStructureEntry, SelectedNodes}; use super::{CursorMoveError, DocumentHistory}; -use crate::application::{GRAPHITE_GIT_COMMIT_HASH, generate_uuid}; +use crate::application::{Environment, GRAPHITE_GIT_COMMIT_HASH, generate_uuid}; use crate::consts::{ ASYMPTOTIC_EFFECT, BLEND_COUNT_PER_LAYER, COLOR_OVERLAY_GRAY, DEFAULT_DOCUMENT_NAME, FILE_EXTENSION, GDD_FILE_EXTENSION, LAYER_INDENT_OFFSET, NODE_CHAIN_WIDTH, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ROTATE_SNAP_INTERVAL, @@ -58,6 +58,7 @@ use std::time::Duration; #[derive(ExtractField)] pub struct DocumentMessageContext<'a> { pub document_id: DocumentId, + pub environment: Environment, pub ipp: &'a InputPreprocessorMessageHandler, pub executor: &'a mut NodeGraphExecutor, pub current_tool: &'a ToolType, @@ -219,6 +220,7 @@ impl MessageHandler> for DocumentMes fn process_message(&mut self, message: DocumentMessage, responses: &mut VecDeque, context: DocumentMessageContext) { let DocumentMessageContext { document_id, + environment, ipp, executor, viewport, @@ -235,6 +237,7 @@ impl MessageHandler> for DocumentMes // Sub-messages DocumentMessage::Navigation(message) => { let context = NavigationMessageContext { + environment, network_interface: &mut self.network_interface, breadcrumb_network_path: &self.breadcrumb_network_path, ipp, diff --git a/editor/src/messages/portfolio/document/navigation/navigation_message_handler.rs b/editor/src/messages/portfolio/document/navigation/navigation_message_handler.rs index 5d5403fad17..fd75284b8fc 100644 --- a/editor/src/messages/portfolio/document/navigation/navigation_message_handler.rs +++ b/editor/src/messages/portfolio/document/navigation/navigation_message_handler.rs @@ -1,4 +1,4 @@ -use crate::application::Editor; +use crate::application::Environment; use crate::consts::{ VIEWPORT_ROTATE_SNAP_INTERVAL, VIEWPORT_SCROLL_RATE, VIEWPORT_ZOOM_LEVELS, VIEWPORT_ZOOM_MIN_FRACTION_COVER, VIEWPORT_ZOOM_MOUSE_RATE, VIEWPORT_ZOOM_SCALE_MAX, VIEWPORT_ZOOM_SCALE_MIN, VIEWPORT_ZOOM_TO_FIT_PADDING_SCALE_FACTOR, @@ -16,6 +16,7 @@ use graph_craft::document::NodeId; #[derive(ExtractField)] pub struct NavigationMessageContext<'a> { + pub environment: Environment, pub network_interface: &'a mut NodeNetworkInterface, pub breadcrumb_network_path: &'a [NodeId], pub ipp: &'a InputPreprocessorMessageHandler, @@ -37,6 +38,7 @@ pub struct NavigationMessageHandler { impl MessageHandler> for NavigationMessageHandler { fn process_message(&mut self, message: NavigationMessage, responses: &mut VecDeque, context: NavigationMessageContext) { let NavigationMessageContext { + environment, network_interface, breadcrumb_network_path, ipp, @@ -176,7 +178,7 @@ impl MessageHandler> for Navigat } NavigationMessage::CanvasPanMouseWheel { use_y_as_x } => { // On Mac, the OS already converts Shift+scroll into horizontal scrolling - let delta = if use_y_as_x && !Editor::environment().is_mac() { + let delta = if use_y_as_x && !environment.is_mac() { (ipp.mouse.scroll_delta.y, 0.).into() } else { ipp.mouse.scroll_delta.as_dvec2() diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 8c3ce17ebed..38ba97c7d8e 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -1,13 +1,14 @@ use super::document::utility_types::document_metadata::LayerNodeIdentifier; use super::persistent_state::{PersistentStateMessage, PersistentStateMessageContext, PersistentStateMessageHandler}; use super::utility_types::PanelType; -use crate::application::{Editor, generate_uuid}; +use crate::application::{Environment, generate_uuid}; use crate::consts::DEFAULT_DOCUMENT_NAME; use crate::messages::animation::TimingInformation; use crate::messages::dialog::simple_dialogs; use crate::messages::frontend::utility_types::{DocumentInfo, PersistedState}; -use crate::messages::input_mapper::utility_types::keyboard::Key; -use crate::messages::input_mapper::utility_types::macros::{action_shortcut, action_shortcut_manual}; +use crate::messages::input_mapper::utility_types::keyboard::{Key, KeysGroup, LabeledShortcut}; +use crate::messages::input_mapper::utility_types::macros::action_shortcut; +use crate::messages::input_mapper::utility_types::misc::ActionShortcut; use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::DocumentMessageContext; use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; @@ -33,6 +34,7 @@ use std::vec; #[derive(ExtractField)] pub struct PortfolioMessageContext<'a> { + pub environment: Environment, pub ipp: &'a InputPreprocessorMessageHandler, pub preferences: &'a PreferencesMessageHandler, pub animation: &'a AnimationMessageHandler, @@ -67,6 +69,7 @@ pub struct PortfolioMessageHandler { impl MessageHandler> for PortfolioMessageHandler { fn process_message(&mut self, message: PortfolioMessage, responses: &mut VecDeque, context: PortfolioMessageContext) { let PortfolioMessageContext { + environment, ipp, preferences, animation, @@ -85,6 +88,7 @@ impl MessageHandler> for Portfolio { let document_inputs = DocumentMessageContext { document_id, + environment, ipp, fonts: &self.fonts, executor: &mut self.executor, @@ -135,9 +139,7 @@ impl MessageHandler> for Portfolio responses.add(PersistentStateMessage::ReadState); // Initialize the frontend with environment information - responses.add(FrontendMessage::UpdatePlatform { - platform: Editor::environment().into(), - }); + responses.add(FrontendMessage::UpdatePlatform { platform: environment.into() }); // Before loading any documents, initially prepare the welcome screen buttons layout responses.add(PortfolioMessage::RequestWelcomeScreenButtonsLayout); @@ -154,16 +156,17 @@ impl MessageHandler> for Portfolio // Request status bar info layout responses.add(PortfolioMessage::RequestStatusBarInfoLayout); - // Send shortcuts for widgets created in the frontend which need shortcut tooltips + // Send shortcuts for widgets created in the frontend which need shortcut tooltips, labeled here since they bypass the layout system + let shortcut = |keys: &[Key]| Some(ActionShortcut::Shortcut(LabeledShortcut::new(KeysGroup(keys.to_vec()), environment))); responses.add(FrontendMessage::SendShortcutFullscreen { - shortcut: action_shortcut_manual!(Key::F11), - shortcut_mac: action_shortcut_manual!(Key::Control, Key::Command, Key::KeyF), + shortcut: shortcut(&[Key::F11]), + shortcut_mac: shortcut(&[Key::Control, Key::Command, Key::KeyF]), }); responses.add(FrontendMessage::SendShortcutAltClick { - shortcut: action_shortcut_manual!(Key::Alt, Key::MouseLeft), + shortcut: shortcut(&[Key::Alt, Key::MouseLeft]), }); responses.add(FrontendMessage::SendShortcutShiftClick { - shortcut: action_shortcut_manual!(Key::Shift, Key::MouseLeft), + shortcut: shortcut(&[Key::Shift, Key::MouseLeft]), }); // Send the information for tooltips and categories for each node/input. @@ -176,6 +179,7 @@ impl MessageHandler> for Portfolio if let Some(document) = self.documents.get_mut(&document_id) { let document_inputs = DocumentMessageContext { document_id, + environment, ipp, fonts: &self.fonts, executor: &mut self.executor, diff --git a/editor/src/messages/tool/utility_types.rs b/editor/src/messages/tool/utility_types.rs index 2f928e3e993..3b256c8e739 100644 --- a/editor/src/messages/tool/utility_types.rs +++ b/editor/src/messages/tool/utility_types.rs @@ -547,7 +547,7 @@ impl HintData { } for shortcut in &hint.key_groups { - widgets.push(ShortcutLabel::new(Some(ActionShortcut::Shortcut(shortcut.clone()))).widget_instance()); + widgets.push(ShortcutLabel::new(Some(ActionShortcut::Keys(shortcut.clone()))).widget_instance()); } if let Some(mouse_movement) = hint.mouse { let mouse_movement = LabeledShortcut(vec![LabeledKeyOrMouseMotion::MouseMotion(mouse_movement)]); @@ -589,10 +589,10 @@ pub struct HintInfo { /// A `KeysGroup` specifies all the keys pressed simultaneously to perform an action (like "Ctrl C" to copy). /// Usually at most one is given, but less commonly, multiple can be used to describe additional hotkeys not used simultaneously (like the four different arrow keys to nudge a layer). #[serde(rename = "keyGroups")] - pub key_groups: Vec, + pub key_groups: Vec, /// `None` means that the regular `key_groups` should be used for all platforms, `Some` is an override for a Mac-only input hint. #[serde(rename = "keyGroupsMac")] - pub key_groups_mac: Option>, + pub key_groups_mac: Option>, /// An optional `MouseMotion` that can indicate the mouse action, like which mouse button is used and whether a drag occurs. /// No such icon is shown if `None` is given, and it can be combined with `key_groups` if desired. pub mouse: Option, @@ -612,7 +612,7 @@ impl HintInfo { pub fn keys(keys: impl IntoIterator, label: impl Into>) -> Self { let keys = keys.into_iter().collect(); Self { - key_groups: vec![KeysGroup(keys).into()], + key_groups: vec![KeysGroup(keys)], key_groups_mac: None, mouse: None, label: label.into(), @@ -626,7 +626,7 @@ impl HintInfo { /// - The four arrow keys can be used to nudge a layer in different directions /// - The G, R, and S keys can be used to enter GRS transformation mode pub fn multi_keys(multi_keys: impl IntoIterator>, label: impl Into>) -> Self { - let key_groups = multi_keys.into_iter().map(|keys| KeysGroup(keys.into_iter().collect()).into()).collect(); + let key_groups = multi_keys.into_iter().map(|keys| KeysGroup(keys.into_iter().collect())).collect(); Self { key_groups, key_groups_mac: None, @@ -662,7 +662,7 @@ impl HintInfo { pub fn keys_and_mouse(keys: impl IntoIterator, mouse_motion: MouseMotion, label: impl Into>) -> Self { let keys = keys.into_iter().collect(); Self { - key_groups: vec![KeysGroup(keys).into()], + key_groups: vec![KeysGroup(keys)], key_groups_mac: None, mouse: Some(mouse_motion), label: label.into(), @@ -672,7 +672,7 @@ impl HintInfo { } pub fn multi_keys_and_mouse(multi_keys: impl IntoIterator>, mouse_motion: MouseMotion, label: impl Into>) -> Self { - let key_groups = multi_keys.into_iter().map(|keys| KeysGroup(keys.into_iter().collect()).into()).collect(); + let key_groups = multi_keys.into_iter().map(|keys| KeysGroup(keys.into_iter().collect())).collect(); Self { key_groups, key_groups_mac: None, @@ -700,7 +700,7 @@ impl HintInfo { pub fn add_mac_keys(mut self, keys: impl IntoIterator) -> Self { let mac_keys = keys.into_iter().collect(); - self.key_groups_mac = Some(vec![KeysGroup(mac_keys).into()]); + self.key_groups_mac = Some(vec![KeysGroup(mac_keys)]); self } } From fcf6747ba348d96ece782b97af4506c0260ade55 Mon Sep 17 00:00:00 2001 From: Timon Date: Fri, 16 Oct 2026 18:06:23 +0000 Subject: [PATCH 46/54] Add editor builder, drop global node runtime --- Cargo.lock | 3 +- desktop/src/app.rs | 71 +++--- desktop/src/event.rs | 4 +- desktop/src/lib.rs | 1 + desktop/src/spawner.rs | 13 ++ desktop/wrapper/src/lib.rs | 44 ++-- editor/Cargo.toml | 4 - editor/src/application.rs | 42 ---- editor/src/builder.rs | 215 ++++++++++++++++++ editor/src/dispatcher.rs | 29 +-- editor/src/lib.rs | 1 + .../messages/future/future_message_handler.rs | 20 +- .../portfolio/portfolio_message_handler.rs | 27 ++- editor/src/node_graph_executor.rs | 32 ++- editor/src/node_graph_executor/runtime.rs | 51 +---- editor/src/node_graph_executor/runtime_io.rs | 44 ---- editor/src/task.rs | 114 +--------- editor/src/test_utils.rs | 28 ++- frontend/wrapper/Cargo.toml | 3 +- frontend/wrapper/src/editor_wrapper.rs | 18 +- frontend/wrapper/src/helpers.rs | 36 ++- frontend/wrapper/src/lib.rs | 7 +- 22 files changed, 433 insertions(+), 374 deletions(-) create mode 100644 desktop/src/spawner.rs create mode 100644 editor/src/builder.rs delete mode 100644 editor/src/node_graph_executor/runtime_io.rs diff --git a/Cargo.lock b/Cargo.lock index 480e390d3cd..cfb9cc47b7b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2398,7 +2398,6 @@ dependencies = [ "serde_bytes", "serde_json", "simplecss", - "spin", "thiserror 2.0.18", "tokio", "tsify", @@ -2406,7 +2405,6 @@ dependencies = [ "usvg", "vello", "wasm-bindgen", - "wasm-bindgen-futures", "web-sys", "wgpu-executor", "zip", @@ -2438,6 +2436,7 @@ dependencies = [ "serde-wasm-bindgen", "serde_bytes", "serde_json", + "spin", "tsify", "wasm-bindgen", "wasm-bindgen-futures", diff --git a/desktop/src/app.rs b/desktop/src/app.rs index 96cfbb1b565..c8c527bef65 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -22,10 +22,11 @@ use crate::input::InputState; use crate::persist; use crate::preferences; use crate::render::{RenderError, RenderState}; +use crate::spawner::TokioSpawner; use crate::ui::{InputEvent, UiCommand, UiInstance}; use crate::window::Window; use crate::wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage, IngestAction, Preferences}; -use crate::wrapper::{DesktopWrapper, FolderStore, MmapResourceStorage, NodeGraphExecutionResult, WgpuContext, serialize_frontend_messages}; +use crate::wrapper::{DesktopWrapper, FolderStore, MmapResourceStorage, WgpuContext, serialize_frontend_messages}; pub(crate) struct App { render_state: Option, @@ -77,36 +78,47 @@ impl App { let exiting = Arc::new(AtomicBool::new(false)); - let rendering_app_event_scheduler = app_event_scheduler.clone(); let (start_render_sender, start_render_receiver) = std::sync::mpsc::sync_channel(1); - let exiting_clone = exiting.clone(); - std::thread::spawn(move || { - let runtime = tokio::runtime::Runtime::new().unwrap(); - loop { - let result = runtime.block_on(DesktopWrapper::execute_node_graph()); - rendering_app_event_scheduler.schedule(AppEvent::NodeGraphExecutionResult(result)); - let _ = start_render_receiver.recv_timeout(Duration::from_millis(10)); - if exiting_clone.load(Ordering::Relaxed) { - break; - } - } - }); + + let async_runtime = tokio::runtime::Builder::new_multi_thread() + .thread_name("graphite-async") + .enable_all() + .build() + .expect("failed to construct tokio runtime"); let resource_storage = MmapResourceStorage::new(dirs::app_resources_dir()).expect("Failed to initialize on-disk resource storage"); - // Wake the winit event loop when an editor future completes. let wake_scheduler = app_event_scheduler.clone(); - let wake = Arc::new(move || { + let editor_wake = move || { wake_scheduler.schedule(AppEvent::DesktopWrapperMessage(DesktopWrapperMessage::Wake)); - }); - let desktop_wrapper = DesktopWrapper::new( + }; + let runtime_wake_sender = start_render_sender.clone(); + let runtime_wake = move || { + let _ = runtime_wake_sender.try_send(()); + }; + let (desktop_wrapper, mut node_runtime) = DesktopWrapper::new( rand::rng().random(), Arc::new(resource_storage), Arc::new(FolderStore::new(dirs::app_autosave_documents_dir())), wgpu_context.clone(), - wake, + TokioSpawner::new(async_runtime.handle().clone()), + editor_wake, + runtime_wake, ); + let rendering_app_event_scheduler = app_event_scheduler.clone(); + let exiting_clone = exiting.clone(); + std::thread::spawn(move || { + loop { + let texture = async_runtime.block_on(node_runtime.run()); + rendering_app_event_scheduler.schedule(AppEvent::ViewportTexture(texture)); + let _ = start_render_receiver.recv_timeout(Duration::from_millis(10)); + if exiting_clone.load(Ordering::Relaxed) { + break; + } + } + }); + Self { render_state: None, wgpu_context, @@ -436,19 +448,16 @@ impl App { } } AppEvent::DesktopWrapperMessage(message) => self.dispatch_desktop_wrapper_message(message), - AppEvent::NodeGraphExecutionResult(result) => match result { - NodeGraphExecutionResult::HasRun(texture) => { - self.dispatch_desktop_wrapper_message(DesktopWrapperMessage::PollNodeGraphEvaluation); - if let Some(texture) = texture - && let Some(render_state) = self.render_state.as_mut() - && let Some(window) = self.window.as_ref() - { - render_state.bind_viewport_texture(texture); - window.request_redraw(); - } + AppEvent::ViewportTexture(texture) => { + self.dispatch_desktop_wrapper_message(DesktopWrapperMessage::PollNodeGraphEvaluation); + if let Some(texture) = texture + && let Some(render_state) = self.render_state.as_mut() + && let Some(window) = self.window.as_ref() + { + render_state.bind_viewport_texture(texture); + window.request_redraw(); } - NodeGraphExecutionResult::NotRun => {} - }, + } AppEvent::UiUpdate(texture) => { if let Some(render_state) = self.render_state.as_mut() { render_state.bind_ui_texture(texture); diff --git a/desktop/src/event.rs b/desktop/src/event.rs index cbe0f77f3b4..87ae4ce8c74 100644 --- a/desktop/src/event.rs +++ b/desktop/src/event.rs @@ -1,5 +1,5 @@ use crate::ui::Cursor; -use crate::wrapper::NodeGraphExecutionResult; +use crate::wrapper::Texture; use crate::wrapper::messages::DesktopWrapperMessage; pub(crate) enum AppEvent { @@ -7,7 +7,7 @@ pub(crate) enum AppEvent { CursorChange(Cursor), WebCommunicationInitialized, DesktopWrapperMessage(DesktopWrapperMessage), - NodeGraphExecutionResult(NodeGraphExecutionResult), + ViewportTexture(Option), ClipboardRead, ClipboardWrite { content: String, diff --git a/desktop/src/lib.rs b/desktop/src/lib.rs index cfff56cd5bf..d4e241dd438 100644 --- a/desktop/src/lib.rs +++ b/desktop/src/lib.rs @@ -22,6 +22,7 @@ mod persist; mod preferences; mod render; mod socket; +mod spawner; mod window; pub(crate) mod consts; diff --git a/desktop/src/spawner.rs b/desktop/src/spawner.rs new file mode 100644 index 00000000000..afdfd077fa5 --- /dev/null +++ b/desktop/src/spawner.rs @@ -0,0 +1,13 @@ +use crate::wrapper::{Spawn, Task}; + +pub(crate) struct TokioSpawner(tokio::runtime::Handle); +impl TokioSpawner { + pub(crate) fn new(handle: tokio::runtime::Handle) -> Self { + Self(handle) + } +} +impl Spawn for TokioSpawner { + fn spawn(&self, task: Task) { + self.0.spawn(task); + } +} diff --git a/desktop/wrapper/src/lib.rs b/desktop/wrapper/src/lib.rs index e84e5e72888..216a3a1be37 100644 --- a/desktop/wrapper/src/lib.rs +++ b/desktop/wrapper/src/lib.rs @@ -1,8 +1,9 @@ use graph_craft::application_io::PlatformApplicationIo; use graph_craft::application_io::resource::ResourceStorage; use graphite_editor::application::{Editor, Environment, Host, Platform}; +use graphite_editor::builder::{Builder, EditorBuilder, RuntimeBuilder}; use graphite_editor::messages::frontend::FrontendMessage; -use graphite_editor::messages::prelude::Wake; +use graphite_editor::node_graph_executor::NodeRuntime; use message_dispatcher::DesktopWrapperMessageDispatcher; use messages::{DesktopFrontendMessage, DesktopWrapperMessage}; @@ -11,6 +12,7 @@ use std::sync::Arc; pub use graph_craft::application_io::resource::MmapResourceStorage; pub use graphite_editor::consts::{DOUBLE_CLICK_MILLISECONDS, FILE_EXTENSION}; pub use graphite_editor::document_store::{DocumentStore, FolderStore}; +pub use graphite_editor::task::{Spawn, Task}; pub use wgpu_executor::Texture; pub use wgpu_executor::WgpuBackends; pub use wgpu_executor::WgpuContext; @@ -33,7 +35,15 @@ pub struct DesktopWrapper { } impl DesktopWrapper { - pub fn new(uuid_random_seed: u64, resource_storage: Arc, document_store: Arc, wgpu_context: WgpuContext, schedule_wake: Wake) -> Self { + pub fn new( + random_seed: u64, + resource_storage: Arc, + document_store: Arc, + wgpu_context: WgpuContext, + spawner: impl Spawn + Send + Sync + 'static, + editor_wake: impl Fn() + Send + Sync + 'static, + runtime_wake: impl Fn() + Send + Sync + 'static, + ) -> (Self, NodeRuntime) { #[cfg(target_os = "windows")] let host = Host::Windows; #[cfg(target_os = "macos")] @@ -43,9 +53,20 @@ impl DesktopWrapper { let env = Environment { platform: Platform::Desktop, host }; let application_io = PlatformApplicationIo::new_with_context(wgpu_context); - Self { - editor: Editor::new(env, uuid_random_seed, resource_storage, document_store, application_io, schedule_wake), - } + let (editor, runtime) = Builder::new() + .with_editor( + EditorBuilder::new() + .with_wake(editor_wake) + .with_environment(env) + .with_resource_storage(resource_storage) + .with_document_store(document_store), + ) + .with_runtime(RuntimeBuilder::new().with_wake(runtime_wake).with_application_io(application_io)) + .with_spawner(spawner) + .with_random_seed(random_seed) + .build(); + + (Self { editor }, runtime) } pub fn dispatch(&mut self, message: DesktopWrapperMessage) -> Vec { @@ -53,19 +74,6 @@ impl DesktopWrapper { executor.queue_desktop_wrapper_message(message); executor.execute() } - - pub async fn execute_node_graph() -> NodeGraphExecutionResult { - let result = graphite_editor::node_graph_executor::run_node_graph().await; - match result { - (true, texture) => NodeGraphExecutionResult::HasRun(texture), - (false, _) => NodeGraphExecutionResult::NotRun, - } - } -} - -pub enum NodeGraphExecutionResult { - HasRun(Option), - NotRun, } pub fn deserialize_editor_message(data: &[u8]) -> Option { diff --git a/editor/Cargo.toml b/editor/Cargo.toml index c97175b15ad..cbce2bad4dc 100644 --- a/editor/Cargo.toml +++ b/editor/Cargo.toml @@ -52,7 +52,6 @@ once_cell = { workspace = true } web-sys = { workspace = true } vello = { workspace = true } base64 = { workspace = true } -spin = { workspace = true } image = { workspace = true } color = { workspace = true } zip = { workspace = true } @@ -68,9 +67,6 @@ wasm-bindgen = { workspace = true, optional = true } [target.'cfg(not(target_family = "wasm"))'.dependencies] tokio = { workspace = true } -[target.'cfg(target_family = "wasm")'.dependencies] -wasm-bindgen-futures = { workspace = true } - [dev-dependencies] # Workspace dependencies env_logger = { workspace = true } diff --git a/editor/src/application.rs b/editor/src/application.rs index 3b1feb9cfaf..e005c67114c 100644 --- a/editor/src/application.rs +++ b/editor/src/application.rs @@ -1,54 +1,12 @@ use crate::dispatcher::Dispatcher; use crate::messages::prelude::*; -use crate::task::TaskExecutor; -use graph_craft::application_io::PlatformApplicationIo; -use graph_craft::application_io::resource::ResourceStorage; pub use graphene_std::uuid::*; -use std::sync::Arc; pub struct Editor { pub dispatcher: Dispatcher, } impl Editor { - pub fn new( - environment: Environment, - uuid_random_seed: u64, - resource_storage: Arc, - document_store: Arc, - mut application_io: PlatformApplicationIo, - wake: Wake, - ) -> Self { - graphene_std::uuid::set_uuid_seed(uuid_random_seed); - - let task_executor = TaskExecutor::new(); - let mut dispatcher = Dispatcher::new(environment, resource_storage, document_store); - dispatcher.message_handlers.future_message_handler = FutureMessageHandler::new(task_executor.handle(), wake.clone()); - application_io.inject_resource_proxy(dispatcher.message_handlers.resource_storage_message_handler.resources()); - crate::node_graph_executor::replace_application_io(application_io); - crate::node_graph_executor::replace_task_executor(task_executor.handle(), wake); - - Self { dispatcher } - } - - #[cfg(test)] - pub(crate) fn new_local_executor() -> (Self, crate::node_graph_executor::NodeRuntime) { - graphene_std::uuid::set_uuid_seed(0); - - let (mut runtime, executor) = crate::node_graph_executor::NodeGraphExecutor::new_with_local_runtime(); - let task_executor = TaskExecutor::blocking(); - let mut dispatcher = Dispatcher::with_executor(executor); - dispatcher.message_handlers.future_message_handler = FutureMessageHandler::new(task_executor.handle(), Arc::new(|| {})); - let editor = Self { dispatcher }; - - let mut application_io = PlatformApplicationIo::default(); - application_io.inject_resource_proxy(editor.dispatcher.message_handlers.resource_storage_message_handler.resources()); - runtime.replace_application_io(application_io); - runtime.replace_task_executor(task_executor.handle(), Arc::new(|| {})); - - (editor, runtime) - } - pub fn handle_message>(&mut self, message: T) -> Vec { self.dispatcher.handle_message(message, true); diff --git a/editor/src/builder.rs b/editor/src/builder.rs new file mode 100644 index 00000000000..7bf6e1fc6e2 --- /dev/null +++ b/editor/src/builder.rs @@ -0,0 +1,215 @@ +use crate::application::{Editor, Environment}; +use crate::dispatcher::Dispatcher; +use crate::messages::prelude::*; +use crate::node_graph_executor::{NodeGraphExecutor, NodeRuntime}; +use crate::task::{Spawn, Spawner}; +use document_container::store::DocumentStore; +use graph_craft::application_io::PlatformApplicationIo; +use graph_craft::application_io::resource::ResourceStorage; +use std::sync::Arc; + +pub type ReadyEditorBuilder = EditorBuilder, Arc>; +pub type ReadyRuntimeBuilder = RuntimeBuilder; + +pub struct Builder { + editor: E, + runtime: R, + spawner: S, + random_seed: N, +} + +impl Builder { + pub fn new() -> Self { + Self { + editor: (), + runtime: (), + spawner: (), + random_seed: (), + } + } +} + +impl Default for Builder { + fn default() -> Self { + Self::new() + } +} + +impl Builder<(), R, S, N> { + pub fn with_editor(self, editor: ReadyEditorBuilder) -> Builder { + Builder { + editor, + runtime: self.runtime, + spawner: self.spawner, + random_seed: self.random_seed, + } + } +} + +impl Builder { + pub fn with_runtime(self, runtime: ReadyRuntimeBuilder) -> Builder { + Builder { + editor: self.editor, + runtime, + spawner: self.spawner, + random_seed: self.random_seed, + } + } +} + +impl Builder { + pub fn with_spawner(self, spawner: impl Spawn + Send + Sync + 'static) -> Builder { + Builder { + editor: self.editor, + runtime: self.runtime, + spawner: Arc::new(spawner), + random_seed: self.random_seed, + } + } +} + +impl Builder { + pub fn with_random_seed(self, random_seed: u64) -> Builder { + Builder { + editor: self.editor, + runtime: self.runtime, + spawner: self.spawner, + random_seed, + } + } +} + +impl Builder { + pub fn build(self) -> (Editor, NodeRuntime) { + let Self { + editor, + runtime, + spawner, + random_seed, + } = self; + let EditorBuilder { + wake: editor_wake, + environment, + resource_storage, + document_store, + } = editor; + let RuntimeBuilder { + wake: runtime_wake, + mut application_io, + } = runtime; + + graphene_std::uuid::set_uuid_seed(random_seed); + + let (executor, mut node_runtime) = NodeGraphExecutor::new(); + + let future_message_handler = FutureMessageHandler::new(spawner.clone(), editor_wake); + let dispatcher = Dispatcher::new(environment, resource_storage, document_store, executor, future_message_handler); + + application_io.inject_resource_proxy(dispatcher.message_handlers.resource_storage_message_handler.resources()); + node_runtime.replace_application_io(application_io); + node_runtime.replace_task_executor(spawner, runtime_wake); + + (Editor { dispatcher }, node_runtime) + } +} + +pub struct EditorBuilder { + wake: W, + environment: E, + resource_storage: R, + document_store: D, +} + +impl EditorBuilder { + pub fn new() -> Self { + Self { + wake: (), + environment: (), + resource_storage: (), + document_store: (), + } + } +} + +impl Default for EditorBuilder { + fn default() -> Self { + Self::new() + } +} + +impl EditorBuilder<(), E, R, D> { + pub fn with_wake(self, wake: impl Fn() + Send + Sync + 'static) -> EditorBuilder { + EditorBuilder { + wake: Arc::new(wake), + environment: self.environment, + resource_storage: self.resource_storage, + document_store: self.document_store, + } + } +} + +impl EditorBuilder { + pub fn with_environment(self, environment: Environment) -> EditorBuilder { + EditorBuilder { + wake: self.wake, + environment, + resource_storage: self.resource_storage, + document_store: self.document_store, + } + } +} + +impl EditorBuilder { + pub fn with_resource_storage(self, resource_storage: Arc) -> EditorBuilder, D> { + EditorBuilder { + wake: self.wake, + environment: self.environment, + resource_storage, + document_store: self.document_store, + } + } +} + +impl EditorBuilder { + pub fn with_document_store(self, document_store: Arc) -> EditorBuilder> { + EditorBuilder { + wake: self.wake, + environment: self.environment, + resource_storage: self.resource_storage, + document_store, + } + } +} + +pub struct RuntimeBuilder { + wake: W, + application_io: A, +} + +impl RuntimeBuilder { + pub fn new() -> Self { + Self { wake: (), application_io: () } + } +} + +impl Default for RuntimeBuilder { + fn default() -> Self { + Self::new() + } +} + +impl RuntimeBuilder<(), A> { + /// Called from any thread when a graph task has completed, to run the runtime again as soon as possible. + pub fn with_wake(self, wake: impl Fn() + Send + Sync + 'static) -> RuntimeBuilder { + RuntimeBuilder { + wake: Arc::new(wake), + application_io: self.application_io, + } + } +} + +impl RuntimeBuilder { + pub fn with_application_io(self, application_io: PlatformApplicationIo) -> RuntimeBuilder { + RuntimeBuilder { wake: self.wake, application_io } + } +} diff --git a/editor/src/dispatcher.rs b/editor/src/dispatcher.rs index 7507551b865..2d3f3ee4e65 100644 --- a/editor/src/dispatcher.rs +++ b/editor/src/dispatcher.rs @@ -7,6 +7,7 @@ use crate::messages::portfolio::utility_types::PanelType; use crate::messages::preferences::preferences_message_handler::PreferencesMessageContext; use crate::messages::prelude::*; use crate::messages::tool::common_functionality::utility_functions::make_path_editable_is_allowed; +use crate::node_graph_executor::NodeGraphExecutor; use graph_craft::application_io::resource::ResourceStorage; use std::sync::Arc; @@ -43,11 +44,11 @@ pub struct DispatcherMessageHandlers { } impl DispatcherMessageHandlers { - fn new(environment: Environment) -> Self { + fn new(environment: Environment, executor: NodeGraphExecutor, document_store: Arc, future_message_handler: FutureMessageHandler) -> Self { Self { animation_message_handler: Default::default(), app_window_message_handler: Default::default(), - future_message_handler: Default::default(), + future_message_handler, broadcast_message_handler: Default::default(), clipboard_message_handler: Default::default(), color_picker_message_handler: Default::default(), @@ -59,7 +60,7 @@ impl DispatcherMessageHandlers { layout_message_handler: Default::default(), menu_bar_message_handler: Default::default(), network_message_handler: Default::default(), - portfolio_message_handler: Default::default(), + portfolio_message_handler: PortfolioMessageHandler::new(executor, document_store), preferences_message_handler: Default::default(), resource_storage_message_handler: Default::default(), tool_message_handler: Default::default(), @@ -101,27 +102,21 @@ const DEBUG_MESSAGE_BLOCK_LIST: &[MessageDiscriminant] = &[ const DEBUG_MESSAGE_ENDING_BLOCK_LIST: &[&str] = &["PointerMove", "PointerOutsideViewport", "Overlays", "Draw", "CurrentTime", "Time"]; impl Dispatcher { - pub fn new(environment: Environment, resource_storage: Arc, document_store: Arc) -> Self { + pub fn new( + environment: Environment, + resource_storage: Arc, + document_store: Arc, + executor: NodeGraphExecutor, + future_message_handler: FutureMessageHandler, + ) -> Self { let mut s = Self { environment, message_queues: Vec::new(), responses: Vec::new(), frontend_update_messages: Vec::new(), - message_handlers: DispatcherMessageHandlers::new(environment), + message_handlers: DispatcherMessageHandlers::new(environment, executor, document_store, future_message_handler), }; s.message_handlers.resource_storage_message_handler = ResourceStorageMessageHandler::new(resource_storage); - s.message_handlers.portfolio_message_handler.set_document_store(document_store); - s - } - - #[cfg(test)] - pub fn with_executor(executor: crate::node_graph_executor::NodeGraphExecutor) -> Self { - let mut s = Self::new( - crate::application::DEFAULT_ENVIRONMENT, - Arc::new(graph_craft::application_io::resource::HashMapResourceStorage::new()), - Arc::new(document_container::store::MemoryStore::default()), - ); - s.message_handlers.portfolio_message_handler = PortfolioMessageHandler::with_executor(executor); s } diff --git a/editor/src/lib.rs b/editor/src/lib.rs index 090d06f94ea..1a1cb35c321 100644 --- a/editor/src/lib.rs +++ b/editor/src/lib.rs @@ -11,6 +11,7 @@ mod macros; extern crate log; pub mod application; +pub mod builder; pub mod consts; pub mod dispatcher; pub mod messages; diff --git a/editor/src/messages/future/future_message_handler.rs b/editor/src/messages/future/future_message_handler.rs index 49c1c1b7f89..780171c7941 100644 --- a/editor/src/messages/future/future_message_handler.rs +++ b/editor/src/messages/future/future_message_handler.rs @@ -6,7 +6,7 @@ use dyn_any::WasmNotSend; use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender, unbounded}; use crate::messages::prelude::*; -use crate::task::{Spawn, TaskExecutor, TaskExecutorHandle}; +use crate::task::Spawner; #[cfg(not(target_family = "wasm"))] type InnerMessageFuture = Pin + Send + 'static>>; @@ -16,10 +16,6 @@ type InnerMessageFuture = Pin + 'static>>; /// Invoked after a result is sent, to wake the platform event loop. pub type Wake = Arc; -fn noop_wake() -> Wake { - Arc::new(|| {}) -} - /// One-shot async work whose result re-enters the dispatcher as a [`Message`]. /// Resolves to [`Message::NoOp`] if polled after the inner future has already been taken. #[derive(Clone, Default)] @@ -68,17 +64,17 @@ pub struct FutureMessageContext {} #[derive(ExtractField)] pub struct FutureMessageHandler { - handle: TaskExecutorHandle, + spawner: Spawner, wake: Wake, results_sender: UnboundedSender, results_receiver: UnboundedReceiver, } impl FutureMessageHandler { - pub fn new(handle: TaskExecutorHandle, wake: Wake) -> Self { + pub fn new(spawner: Spawner, wake: Wake) -> Self { let (results_sender, results_receiver) = unbounded(); Self { - handle, + spawner, wake, results_sender, results_receiver, @@ -93,12 +89,6 @@ impl FutureMessageHandler { } } -impl Default for FutureMessageHandler { - fn default() -> Self { - Self::new(TaskExecutor::blocking().handle(), noop_wake()) - } -} - impl std::fmt::Debug for FutureMessageHandler { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("FutureMessageHandler").finish_non_exhaustive() @@ -112,7 +102,7 @@ impl MessageHandler for FutureMessageHandle FutureMessage::Await { future } => { let results = self.results_sender.clone(); let wake = self.wake.clone(); - self.handle.spawn(Box::pin(async move { + self.spawner.spawn(Box::pin(async move { let message = future.await; let _ = results.unbounded_send(message); wake(); diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 38ba97c7d8e..bed2d19c5a2 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -45,7 +45,7 @@ pub struct PortfolioMessageContext<'a> { pub resource_storage: &'a ResourceStorageMessageHandler, } -#[derive(Debug, Default, ExtractField)] +#[derive(Debug, ExtractField)] pub struct PortfolioMessageHandler { pub documents: HashMap, loading_documents: HashMap, @@ -1033,12 +1033,25 @@ impl MessageHandler> for Portfolio } impl PortfolioMessageHandler { - pub fn with_executor(executor: crate::node_graph_executor::NodeGraphExecutor) -> Self { - Self { executor, ..Default::default() } - } - - pub fn set_document_store(&mut self, store: Arc) { - self.document_store = DocumentStoreHandle::new(store); + pub fn new(executor: NodeGraphExecutor, document_store: Arc) -> Self { + Self { + documents: Default::default(), + loading_documents: Default::default(), + document_ids: Default::default(), + active_document_id: Default::default(), + failed_documents: Default::default(), + persistent_state: Default::default(), + fonts: Default::default(), + ingest: Default::default(), + executor, + selection_mode: Default::default(), + reset_node_definitions_on_open: Default::default(), + workspace: Default::default(), + document_store: DocumentStoreHandle::new(document_store), + pending_opens: Default::default(), + restore_select: Default::default(), + store_unavailable: Default::default(), + } } #[cfg(test)] diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index 940a094c8dd..4a6231627b9 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -20,9 +20,7 @@ use graphene_std::{ATTR_TRANSFORM, Context, Graphic}; use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypesDelta; use std::any::Any; use std::sync::Arc; - -mod runtime_io; -pub use runtime_io::NodeRuntimeIO; +use std::sync::mpsc::{Receiver, Sender}; mod runtime; pub use runtime::*; @@ -66,9 +64,10 @@ pub enum NodeGraphUpdate { EyedropperPreview(Raster), } -#[derive(Debug, Default)] +#[derive(Debug)] pub struct NodeGraphExecutor { - runtime_io: NodeRuntimeIO, + sender: Sender, + receiver: Receiver, current_execution_id: u64, futures: VecDeque<(u64, ExecutionContext)>, node_graph_hash: u64, @@ -110,23 +109,22 @@ struct GradientMigration { } impl NodeGraphExecutor { - /// A local runtime is useful on threads since having global state causes flakes - #[cfg(test)] - pub(crate) fn new_with_local_runtime() -> (NodeRuntime, Self) { + pub(crate) fn new() -> (Self, NodeRuntime) { let (request_sender, request_receiver) = std::sync::mpsc::channel(); let (response_sender, response_receiver) = std::sync::mpsc::channel(); let node_runtime = NodeRuntime::new(request_receiver, response_sender); let node_executor = Self { futures: Default::default(), - runtime_io: NodeRuntimeIO::with_channels(request_sender, response_receiver), + sender: request_sender, + receiver: response_receiver, node_graph_hash: 0, current_execution_id: 0, previous_node_to_inspect: Vec::new(), gradient_migration: None, gradient_migration_attempted: HashSet::new(), }; - (node_runtime, node_executor) + (node_executor, node_runtime) } /// Execute the network by flattening it and creating a borrow stack. @@ -134,13 +132,13 @@ impl NodeGraphExecutor { let execution_id = self.current_execution_id; self.current_execution_id += 1; let request = ExecutionRequest { execution_id, render_config }; - self.runtime_io.send(GraphRuntimeRequest::ExecutionRequest(request)).expect("Failed to send generation request"); + self.sender.send(GraphRuntimeRequest::ExecutionRequest(request)).expect("Failed to send generation request"); execution_id } pub fn update_editor_preferences(&self, editor_preferences: EditorPreferences) { - self.runtime_io + self.sender .send(GraphRuntimeRequest::EditorPreferencesUpdate(editor_preferences)) .expect("Failed to send editor preferences"); } @@ -155,7 +153,7 @@ impl NodeGraphExecutor { let resources = document.resources.registry.clone(); - self.runtime_io + self.sender .send(GraphRuntimeRequest::GraphUpdate(GraphUpdate { network, resources, @@ -176,7 +174,7 @@ impl NodeGraphExecutor { let resources = document.resources.registry.clone(); - self.runtime_io + self.sender .send(GraphRuntimeRequest::GraphUpdate(GraphUpdate { network, resources, node_to_inspect })) .map_err(|e| e.to_string())?; } @@ -340,7 +338,7 @@ impl NodeGraphExecutor { export_config.size = resolution; // Execute the node graph - self.runtime_io + self.sender .send(GraphRuntimeRequest::GraphUpdate(GraphUpdate { network, resources, @@ -367,7 +365,7 @@ impl NodeGraphExecutor { self.cancel_gradient_migration(); } - let results = self.runtime_io.receive().collect::>(); + let results = self.receiver.try_iter().collect::>(); for response in results { match response { NodeGraphUpdate::ExecutionResponse(execution_response) => { @@ -579,7 +577,7 @@ impl NodeGraphExecutor { let resources = document.resources.registry.clone(); if self - .runtime_io + .sender .send(GraphRuntimeRequest::GraphUpdate(GraphUpdate { network, resources, diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index 048df43a248..e5e279f0a68 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -1,6 +1,6 @@ use super::*; use crate::messages::frontend::utility_types::{ExportBounds, FileType}; -use crate::task::{Spawn, Task, TaskExecutorHandle}; +use crate::task::{Spawn, Spawner, Task}; use glam::{DAffine2, DVec2, UVec2}; use graph_craft::application_io::resource::ResourceRegistry; use graph_craft::application_io::{PlatformApplicationIo, PlatformEditorApi}; @@ -23,9 +23,8 @@ use graphene_std::transform::RenderQuality; use graphene_std::vector::Vector; use graphene_std::vector::style::RenderMode; use graphene_std::{Artboard, Context, Graphic, NodeError, Poll}; -use interpreted_executor::dynamic_executor::{DynamicExecutor, IntrospectError, ResolvedDocumentNodeTypesDelta}; +use interpreted_executor::dynamic_executor::{DynamicExecutor, ResolvedDocumentNodeTypesDelta}; use interpreted_executor::util::wrap_network_in_scope; -use spin::Mutex; use std::sync::Arc; use std::sync::mpsc::{Receiver, Sender}; @@ -96,9 +95,6 @@ pub struct ExportConfig { pub artboard_count: usize, } -// TODO: Replace with `core::cell::LazyCell` () or similar -pub static NODE_RUNTIME: once_cell::sync::Lazy>> = once_cell::sync::Lazy::new(|| Mutex::new(None)); - impl NodeRuntime { pub fn new(receiver: Receiver, sender: Sender) -> Self { Self { @@ -386,7 +382,7 @@ impl NodeRuntime { } /// Updates state data - pub fn process_monitor_nodes(&mut self, responses: &mut VecDeque, update_thumbnails: bool) { + fn process_monitor_nodes(&mut self, responses: &mut VecDeque, update_thumbnails: bool) { // TODO: Consider optimizing this since it's currently O(m*n^2), with a sort it could be made O(m * n*log(n)) self.thumbnail_renders.retain(|id, _| self.monitor_nodes.iter().any(|monitor_node_path| monitor_node_path.contains(id))); @@ -566,39 +562,6 @@ fn expand_to_thumbnail_aspect(bounds: [DVec2; 2]) -> [DVec2; 2] { [center - half, center + half] } -pub async fn introspect_node(path: &[NodeId]) -> Result, IntrospectError> { - let runtime = NODE_RUNTIME.lock(); - if let Some(ref mut runtime) = runtime.as_ref() { - return runtime.executor.introspect(path); - } - Err(IntrospectError::RuntimeNotReady) -} - -pub async fn run_node_graph() -> (bool, Option) { - let Some(mut runtime) = NODE_RUNTIME.try_lock() else { return (false, None) }; - if let Some(ref mut runtime) = runtime.as_mut() { - return (true, runtime.run().await); - } - (false, None) -} - -pub fn replace_node_runtime(runtime: NodeRuntime) -> Option { - let mut node_runtime = NODE_RUNTIME.lock(); - node_runtime.replace(runtime) -} -pub(crate) fn replace_application_io(application_io: PlatformApplicationIo) { - let mut node_runtime = NODE_RUNTIME.lock(); - if let Some(node_runtime) = &mut *node_runtime { - node_runtime.replace_application_io(application_io); - } -} -pub(crate) fn replace_task_executor(handle: TaskExecutorHandle, wake: Wake) { - let mut node_runtime = NODE_RUNTIME.lock(); - if let Some(node_runtime) = &mut *node_runtime { - node_runtime.replace_task_executor(handle, wake); - } -} - impl NodeRuntime { pub(crate) fn replace_application_io(&mut self, application_io: PlatformApplicationIo) { self.editor_api = PlatformEditorApi { @@ -608,9 +571,9 @@ impl NodeRuntime { .into(); } - pub(crate) fn replace_task_executor(&mut self, handle: TaskExecutorHandle, wake: Wake) { + pub(crate) fn replace_task_executor(&mut self, spawner: Spawner, wake: Wake) { let spawn = NodeTaskExecutor { - handle, + spawner, sender: self.sender.clone(), wake, }; @@ -623,7 +586,7 @@ impl NodeRuntime { } struct NodeTaskExecutor { - handle: TaskExecutorHandle, + spawner: Spawner, sender: Sender, wake: Wake, } @@ -632,7 +595,7 @@ impl Spawn for NodeTaskExecutor { fn spawn(&self, task: Task) { let sender = self.sender.clone(); let wake = self.wake.clone(); - self.handle.spawn(Box::pin(async move { + self.spawner.spawn(Box::pin(async move { task.await; let _ = sender.send(NodeGraphUpdate::TaskCompleted); wake(); diff --git a/editor/src/node_graph_executor/runtime_io.rs b/editor/src/node_graph_executor/runtime_io.rs deleted file mode 100644 index 5b2235ae5ea..00000000000 --- a/editor/src/node_graph_executor/runtime_io.rs +++ /dev/null @@ -1,44 +0,0 @@ -use super::*; -use std::sync::mpsc::{Receiver, Sender}; - -/// Handles communication with the NodeRuntime -#[derive(Debug)] -pub struct NodeRuntimeIO { - // Send to - sender: Sender, - receiver: Receiver, -} - -impl Default for NodeRuntimeIO { - fn default() -> Self { - Self::new() - } -} - -impl NodeRuntimeIO { - /// Creates a new NodeRuntimeIO instance - pub fn new() -> Self { - let (response_sender, response_receiver) = std::sync::mpsc::channel(); - let (request_sender, request_receiver) = std::sync::mpsc::channel(); - replace_node_runtime(NodeRuntime::new(request_receiver, response_sender)); - - Self { - sender: request_sender, - receiver: response_receiver, - } - } - #[cfg(test)] - pub fn with_channels(sender: Sender, receiver: Receiver) -> Self { - Self { sender, receiver } - } - - /// Sends a message to the NodeRuntime - pub fn send(&self, message: GraphRuntimeRequest) -> Result<(), String> { - self.sender.send(message).map_err(|e| e.to_string()) - } - - /// Receives any pending updates from the NodeRuntime - pub fn receive(&self) -> impl Iterator + use<'_> { - self.receiver.try_iter() - } -} diff --git a/editor/src/task.rs b/editor/src/task.rs index 802b5c4c559..f571fb1c94d 100644 --- a/editor/src/task.rs +++ b/editor/src/task.rs @@ -1,116 +1,4 @@ pub use graphene_std::runtime::{Spawn, Task}; use std::sync::Arc; -#[derive(Clone)] -pub struct TaskExecutorHandle { - backend: Arc, -} - -impl Spawn for TaskExecutorHandle { - fn spawn(&self, task: Task) { - self.backend.spawn(task); - } -} - -pub struct TaskExecutor { - backend: Arc, -} - -impl TaskExecutor { - pub fn new() -> Self { - let backend = { - #[cfg(target_family = "wasm")] - { - Arc::new(Backend::Wasm(WasmSpawner)) - } - #[cfg(not(target_family = "wasm"))] - { - Arc::new(Backend::Tokio(TokioSpawner::new())) - } - }; - Self { backend } - } - pub fn blocking() -> Self { - let backend = Arc::new(Backend::Blocking(BlockingSpawner)); - Self { backend } - } - pub fn handle(&self) -> TaskExecutorHandle { - TaskExecutorHandle { backend: self.backend.clone() } - } -} - -impl Default for TaskExecutor { - fn default() -> Self { - Self::new() - } -} - -enum Backend { - #[cfg(target_family = "wasm")] - Wasm(WasmSpawner), - #[cfg(not(target_family = "wasm"))] - Tokio(TokioSpawner), - Blocking(BlockingSpawner), -} - -impl Spawn for Backend { - fn spawn(&self, task: Task) { - match self { - #[cfg(target_family = "wasm")] - Backend::Wasm(spawner) => spawner.spawn(task), - #[cfg(not(target_family = "wasm"))] - Backend::Tokio(spawner) => spawner.spawn(task), - Backend::Blocking(spawner) => spawner.spawn(task), - } - } -} - -#[cfg(target_family = "wasm")] -struct WasmSpawner; - -#[cfg(target_family = "wasm")] -impl Spawn for WasmSpawner { - fn spawn(&self, task: Task) { - wasm_bindgen_futures::spawn_local(task); - } -} - -#[cfg(not(target_family = "wasm"))] -struct TokioSpawner(Option); - -#[cfg(not(target_family = "wasm"))] -impl TokioSpawner { - fn new() -> Self { - let runtime = tokio::runtime::Builder::new_multi_thread() - .worker_threads(1) - .thread_name("graphite-async") - .enable_all() - .build() - .expect("failed to construct the task tokio runtime"); - Self(Some(runtime)) - } -} - -#[cfg(not(target_family = "wasm"))] -impl Spawn for TokioSpawner { - fn spawn(&self, task: Task) { - self.0.as_ref().expect("the runtime lives until drop").spawn(task); - } -} - -#[cfg(not(target_family = "wasm"))] -impl Drop for TokioSpawner { - fn drop(&mut self) { - if let Some(runtime) = self.0.take() { - runtime.shutdown_background(); - } - } -} - -struct BlockingSpawner; - -impl Spawn for BlockingSpawner { - fn spawn(&self, task: Task) { - futures::executor::block_on(task); - } -} +pub type Spawner = Arc; diff --git a/editor/src/test_utils.rs b/editor/src/test_utils.rs index 532ff6d24e7..956db79516e 100644 --- a/editor/src/test_utils.rs +++ b/editor/src/test_utils.rs @@ -1,4 +1,5 @@ -use crate::application::Editor; +use crate::application::{DEFAULT_ENVIRONMENT, Editor}; +use crate::builder::{Builder, EditorBuilder, RuntimeBuilder}; use crate::messages::input_mapper::utility_types::keyboard::ModifierKeys; use crate::messages::input_mapper::utility_types::pointer::{EditorPointerState, MouseKeys, ViewportPosition}; use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier; @@ -8,10 +9,15 @@ use crate::messages::tool::tool_messages::tool_prelude::Key; use crate::messages::tool::utility_types::ToolType; use crate::node_graph_executor::Instrumented; use crate::node_graph_executor::NodeRuntime; +use crate::task::{Spawn, Task}; use crate::test_utils::test_prelude::LayerNodeIdentifier; +use document_container::store::MemoryStore; use glam::{DVec2, UVec2}; +use graph_craft::application_io::PlatformApplicationIo; +use graph_craft::application_io::resource::HashMapResourceStorage; use graphene_std::raster::color::Color; use graphene_std::uuid::NodeId; +use std::sync::Arc; /// A set of utility functions to make the writing of editor test more declarative pub struct EditorTestUtils { @@ -23,7 +29,18 @@ impl EditorTestUtils { pub fn create() -> Self { let _ = env_logger::builder().is_test(true).try_init(); - let (mut editor, runtime) = Editor::new_local_executor(); + let (mut editor, runtime) = Builder::new() + .with_editor( + EditorBuilder::new() + .with_wake(|| {}) + .with_environment(DEFAULT_ENVIRONMENT) + .with_resource_storage(Arc::new(HashMapResourceStorage::new())) + .with_document_store(Arc::new(MemoryStore::default())), + ) + .with_runtime(RuntimeBuilder::new().with_wake(|| {}).with_application_io(PlatformApplicationIo::default())) + .with_spawner(BlockingSpawner) + .with_random_seed(0) + .build(); editor.handle_message(PortfolioMessage::Init); @@ -347,6 +364,13 @@ impl FrontendMessageTestUtils for FrontendMessage { } } +struct BlockingSpawner; +impl Spawn for BlockingSpawner { + fn spawn(&self, task: Task) { + futures::executor::block_on(task); + } +} + #[cfg(test)] pub mod test_prelude { pub use super::FrontendMessageTestUtils; diff --git a/frontend/wrapper/Cargo.toml b/frontend/wrapper/Cargo.toml index 6b9fca2fa92..8574c2b23c3 100644 --- a/frontend/wrapper/Cargo.toml +++ b/frontend/wrapper/Cargo.toml @@ -13,7 +13,7 @@ readme = "./README.md" [features] default = ["gpu", "shader-nodes", "web"] web = ["editor", "editor?/wasm"] -editor = ["dep:editor", "dep:graphene-std", "dep:graph-craft", "dep:wgpu"] +editor = ["dep:editor", "dep:graphene-std", "dep:graph-craft", "dep:spin", "dep:wgpu"] gpu = ["editor?/gpu"] shader-nodes = ["graphene-std?/shader-nodes", "gpu"] native = [] @@ -38,6 +38,7 @@ wasm-bindgen-futures = { workspace = true } wgpu = { workspace = true, optional = true } web-sys = { workspace = true } serde_json = { workspace = true } +spin = { workspace = true, optional = true } tsify = { workspace = true } [dev-dependencies] diff --git a/frontend/wrapper/src/editor_wrapper.rs b/frontend/wrapper/src/editor_wrapper.rs index 4126d6c8a5e..3f2f28e7ae6 100644 --- a/frontend/wrapper/src/editor_wrapper.rs +++ b/frontend/wrapper/src/editor_wrapper.rs @@ -16,7 +16,7 @@ use crate::helpers::{calculate_hash, render_image_data_to_canvases}; use crate::helpers::{request_animation_frame, set_timeout}; use crate::{EDITOR_HAS_CRASHED, FRONTEND_READY}; #[cfg(all(not(feature = "native"), target_family = "wasm"))] -use editor::application::{Editor, Environment, Host, Platform}; +use editor::application::{Environment, Host, Platform}; #[cfg(feature = "editor")] use editor::messages::prelude::*; #[cfg(feature = "editor")] @@ -54,6 +54,7 @@ impl EditorWrapper { impl EditorWrapper { #[cfg(all(not(feature = "native"), target_family = "wasm"))] pub async fn create(platform: String, uuid_random_seed: u64, frontend_message_handler_callback: js_sys::Function) -> EditorWrapper { + use editor::builder::{Builder, EditorBuilder, RuntimeBuilder}; use graph_craft::application_io::PlatformApplicationIo; use graph_craft::application_io::resource::*; @@ -73,13 +74,24 @@ impl EditorWrapper { }; let application_io = PlatformApplicationIo::new().await; - let wake = crate::helpers::async_wake_callback(); let document_store = std::sync::Arc::new(editor::document_store::OpfsStore::new("documents")); - let editor = Editor::new(Environment { platform: Platform::Web, host }, uuid_random_seed, storage, document_store, application_io, wake); + let (editor, runtime) = Builder::new() + .with_editor( + EditorBuilder::new() + .with_wake(crate::helpers::wake_editor) + .with_environment(Environment { platform: Platform::Web, host }) + .with_resource_storage(storage) + .with_document_store(document_store), + ) + .with_runtime(RuntimeBuilder::new().with_wake(crate::helpers::wake_runtime).with_application_io(application_io)) + .with_spawner(crate::helpers::WasmSpawner) + .with_random_seed(uuid_random_seed) + .build(); if EDITOR.with(|slot| slot.lock().ok().map(|mut guard| *guard = Some(editor))).is_none() { log::error!("Attempted to initialize the editor more than once"); } + *crate::NODE_RUNTIME.lock() = Some(runtime); Self::initialize_wrapper(frontend_message_handler_callback) } diff --git a/frontend/wrapper/src/helpers.rs b/frontend/wrapper/src/helpers.rs index a808874e9e9..c7958ef7ba2 100644 --- a/frontend/wrapper/src/helpers.rs +++ b/frontend/wrapper/src/helpers.rs @@ -11,6 +11,8 @@ use editor::application::Editor; use editor::messages::input_mapper::utility_types::keyboard::Key; #[cfg(not(feature = "native"))] use editor::messages::prelude::*; +#[cfg(all(not(feature = "native"), target_family = "wasm"))] +use editor::task::{Spawn, Task}; use js_sys::{Object, Reflect}; #[cfg(not(feature = "native"))] use std::sync::atomic::Ordering; @@ -81,8 +83,10 @@ pub(crate) async fn poll_node_graph_evaluation() { return; } - if !editor::node_graph_executor::run_node_graph().await.0 { - return; + { + let Some(mut runtime) = crate::NODE_RUNTIME.try_lock() else { return }; + let Some(runtime) = runtime.as_mut() else { return }; + runtime.run().await; } editor_and_wrapper(|editor, wrapper| { @@ -112,15 +116,27 @@ pub(crate) async fn poll_node_graph_evaluation() { }); } -/// Web wake callback: queues a microtask that dispatches [`FutureMessage::Wake`]. #[cfg(all(not(feature = "native"), target_family = "wasm"))] -pub(crate) fn async_wake_callback() -> Wake { - use std::sync::Arc; - Arc::new(|| { - wasm_bindgen_futures::spawn_local(async { - wrapper(|wrapper| wrapper.dispatch(FutureMessage::Wake)); - }); - }) +pub(crate) struct WasmSpawner; +#[cfg(all(not(feature = "native"), target_family = "wasm"))] +impl Spawn for WasmSpawner { + fn spawn(&self, task: Task) { + wasm_bindgen_futures::spawn_local(task); + } +} + +/// Web editor wake: queues a microtask that dispatches [`FutureMessage::Wake`]. +#[cfg(all(not(feature = "native"), target_family = "wasm"))] +pub(crate) fn wake_editor() { + wasm_bindgen_futures::spawn_local(async { + wrapper(|wrapper| wrapper.dispatch(FutureMessage::Wake)); + }); +} + +/// Web runtime wake: queues a run of the node runtime. Used to wake the runtime on async task completion. +#[cfg(all(not(feature = "native"), target_family = "wasm"))] +pub(crate) fn wake_runtime() { + wasm_bindgen_futures::spawn_local(poll_node_graph_evaluation()); } /// Blits each rasterized image to a canvas registered on `window.imageCanvases` diff --git a/frontend/wrapper/src/lib.rs b/frontend/wrapper/src/lib.rs index 0785d8efab5..a38892db0ba 100644 --- a/frontend/wrapper/src/lib.rs +++ b/frontend/wrapper/src/lib.rs @@ -27,6 +27,8 @@ pub static EDITOR_HAS_CRASHED: AtomicBool = AtomicBool::new(false); pub static FRONTEND_READY: AtomicBool = AtomicBool::new(false); pub static NODE_GRAPH_ERROR_DISPLAYED: AtomicBool = AtomicBool::new(false); pub static LOGGER: WasmLog = WasmLog; +#[cfg(not(feature = "native"))] +pub static NODE_RUNTIME: spin::Mutex> = spin::Mutex::new(None); thread_local! { #[cfg(not(feature = "native"))] @@ -60,8 +62,9 @@ pub fn panic_hook(info: &panic::PanicHookInfo) { log::error!("Node graph evaluation panicked {info}"); // When the graph panics, the node runtime lock may not be released properly - if editor::node_graph_executor::NODE_RUNTIME.try_lock().is_none() { - unsafe { editor::node_graph_executor::NODE_RUNTIME.force_unlock() }; + #[cfg(not(feature = "native"))] + if NODE_RUNTIME.try_lock().is_none() { + unsafe { NODE_RUNTIME.force_unlock() }; } if !NODE_GRAPH_ERROR_DISPLAYED.load(Ordering::SeqCst) { From 20034b6965a96fc4c4b387e25f5fec2ed4be48dc Mon Sep 17 00:00:00 2001 From: Timon Date: Fri, 16 Oct 2026 18:06:23 +0000 Subject: [PATCH 47/54] Add Wake message, rerun graph when tasks finish --- desktop/src/app.rs | 1 - .../src/handle_desktop_wrapper_message.rs | 2 +- editor/src/builder.rs | 2 +- editor/src/dispatcher.rs | 18 +++---- editor/src/messages/future/future_message.rs | 3 +- .../messages/future/future_message_handler.rs | 21 +++----- editor/src/messages/future/mod.rs | 2 +- editor/src/messages/message.rs | 1 + .../document/storage_tests/lifecycle_tests.rs | 2 +- .../portfolio/portfolio_message_handler.rs | 8 +-- editor/src/messages/prelude.rs | 2 +- editor/src/node_graph_executor.rs | 33 ++++++------ editor/src/node_graph_executor/runtime.rs | 52 ++++++++++++++++--- editor/src/test_utils.rs | 27 +++++++--- frontend/wrapper/src/helpers.rs | 4 +- 15 files changed, 108 insertions(+), 70 deletions(-) diff --git a/desktop/src/app.rs b/desktop/src/app.rs index c8c527bef65..6b0ea29583e 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -449,7 +449,6 @@ impl App { } AppEvent::DesktopWrapperMessage(message) => self.dispatch_desktop_wrapper_message(message), AppEvent::ViewportTexture(texture) => { - self.dispatch_desktop_wrapper_message(DesktopWrapperMessage::PollNodeGraphEvaluation); if let Some(texture) = texture && let Some(render_state) = self.render_state.as_mut() && let Some(window) = self.window.as_ref() diff --git a/desktop/wrapper/src/handle_desktop_wrapper_message.rs b/desktop/wrapper/src/handle_desktop_wrapper_message.rs index df3237623af..072869017a9 100644 --- a/desktop/wrapper/src/handle_desktop_wrapper_message.rs +++ b/desktop/wrapper/src/handle_desktop_wrapper_message.rs @@ -10,7 +10,7 @@ pub(super) fn handle_desktop_wrapper_message(dispatcher: &mut DesktopWrapperMess dispatcher.queue_editor_message(*message); } DesktopWrapperMessage::Wake => { - dispatcher.queue_editor_message(EditorMessage::Future(FutureMessage::Wake)); + dispatcher.queue_editor_message(EditorMessage::Wake); } DesktopWrapperMessage::Input(message) => { dispatcher.queue_editor_message(EditorMessage::InputPreprocessor(message)); diff --git a/editor/src/builder.rs b/editor/src/builder.rs index 7bf6e1fc6e2..f3bb6fe513b 100644 --- a/editor/src/builder.rs +++ b/editor/src/builder.rs @@ -100,7 +100,7 @@ impl Builder { graphene_std::uuid::set_uuid_seed(random_seed); - let (executor, mut node_runtime) = NodeGraphExecutor::new(); + let (executor, mut node_runtime) = NodeGraphExecutor::new(editor_wake.clone()); let future_message_handler = FutureMessageHandler::new(spawner.clone(), editor_wake); let dispatcher = Dispatcher::new(environment, resource_storage, document_store, executor, future_message_handler); diff --git a/editor/src/dispatcher.rs b/editor/src/dispatcher.rs index 2d3f3ee4e65..31228097599 100644 --- a/editor/src/dispatcher.rs +++ b/editor/src/dispatcher.rs @@ -145,13 +145,6 @@ impl Dispatcher { pub fn handle_message>(&mut self, message: T, process_after_all_current: bool) { let message = message.into(); - // Drain async results into the queue before processing the new message. - let mut async_results = VecDeque::new(); - self.message_handlers.future_message_handler.drain_results(&mut async_results); - if !async_results.is_empty() { - Self::schedule_execution(&mut self.message_queues, true, async_results); - } - // If we are not maintaining the buffer, simply add to the current queue Self::schedule_execution(&mut self.message_queues, process_after_all_current, [message]); @@ -202,7 +195,7 @@ impl Dispatcher { self.message_handlers.app_window_message_handler.process_message(message, &mut queue, ()); } Message::Future(message) => { - self.message_handlers.future_message_handler.process_message(message, &mut queue, FutureMessageContext {}); + self.message_handlers.future_message_handler.process_message(message, &mut queue, ()); } Message::Broadcast(message) => self.message_handlers.broadcast_message_handler.process_message(message, &mut queue, ()), Message::Clipboard(message) => { @@ -367,10 +360,17 @@ impl Dispatcher { Message::Viewport(message) => { self.message_handlers.viewport_message_handler.process_message(message, &mut queue, ()); } - Message::NoOp => {} + // Messages + Message::Wake => { + if let Err(error) = self.poll_node_graph_evaluation(&mut queue) { + log::error!("Error polling node graph: {error}"); + } + queue.add(FutureMessage::Drain); + } Message::Batched { messages } => { messages.into_iter().for_each(|message| self.handle_message(message, false)); } + Message::NoOp => {} } // If there are child messages, append the queue to the list of queues diff --git a/editor/src/messages/future/future_message.rs b/editor/src/messages/future/future_message.rs index 99ccb6052ec..612e3713140 100644 --- a/editor/src/messages/future/future_message.rs +++ b/editor/src/messages/future/future_message.rs @@ -11,6 +11,5 @@ pub enum FutureMessage { #[derivative(Debug = "ignore", PartialEq = "ignore")] future: MessageFuture, }, - /// Sent by a wake callback to nudge an idle event loop into a dispatch tick. - Wake, + Drain, } diff --git a/editor/src/messages/future/future_message_handler.rs b/editor/src/messages/future/future_message_handler.rs index 780171c7941..cc766c2aa99 100644 --- a/editor/src/messages/future/future_message_handler.rs +++ b/editor/src/messages/future/future_message_handler.rs @@ -59,9 +59,6 @@ where } } -#[derive(ExtractField)] -pub struct FutureMessageContext {} - #[derive(ExtractField)] pub struct FutureMessageHandler { spawner: Spawner, @@ -80,13 +77,6 @@ impl FutureMessageHandler { results_receiver, } } - - /// Pull every resolved async result into `out`. - pub fn drain_results(&mut self, out: &mut VecDeque) { - while let Ok(Some(message)) = self.results_receiver.try_next() { - out.push_back(message); - } - } } impl std::fmt::Debug for FutureMessageHandler { @@ -96,8 +86,8 @@ impl std::fmt::Debug for FutureMessageHandler { } #[message_handler_data] -impl MessageHandler for FutureMessageHandler { - fn process_message(&mut self, message: FutureMessage, responses: &mut VecDeque, _context: FutureMessageContext) { +impl MessageHandler for FutureMessageHandler { + fn process_message(&mut self, message: FutureMessage, responses: &mut VecDeque, _context: ()) { match message { FutureMessage::Await { future } => { let results = self.results_sender.clone(); @@ -107,10 +97,11 @@ impl MessageHandler for FutureMessageHandle let _ = results.unbounded_send(message); wake(); })); - self.drain_results(responses); } - FutureMessage::Wake => { - // Tick-only message: the dispatcher's top-of-tick drain handles the real work. + FutureMessage::Drain => { + while let Ok(Some(message)) = self.results_receiver.try_next() { + responses.push_back(message); + } } } } diff --git a/editor/src/messages/future/mod.rs b/editor/src/messages/future/mod.rs index 20981f5d09e..8767de3b6f8 100644 --- a/editor/src/messages/future/mod.rs +++ b/editor/src/messages/future/mod.rs @@ -4,4 +4,4 @@ mod future_message_handler; #[doc(inline)] pub use future_message::{FutureMessage, FutureMessageDiscriminant}; #[doc(inline)] -pub use future_message_handler::{FutureMessageContext, FutureMessageHandler, MessageFuture, Wake}; +pub use future_message_handler::{FutureMessageHandler, MessageFuture, Wake}; diff --git a/editor/src/messages/message.rs b/editor/src/messages/message.rs index e3e3e6ec3a9..2233f05be9b 100644 --- a/editor/src/messages/message.rs +++ b/editor/src/messages/message.rs @@ -47,6 +47,7 @@ pub enum Message { Viewport(ViewportMessage), // Messages + Wake, Batched { messages: Box<[Message]>, }, diff --git a/editor/src/messages/portfolio/document/storage_tests/lifecycle_tests.rs b/editor/src/messages/portfolio/document/storage_tests/lifecycle_tests.rs index fa6e4ec8176..7031a686d4a 100644 --- a/editor/src/messages/portfolio/document/storage_tests/lifecycle_tests.rs +++ b/editor/src/messages/portfolio/document/storage_tests/lifecycle_tests.rs @@ -7,7 +7,7 @@ use crate::test_utils::EditorTestUtils; use document_container::{AnyContainer, AsyncContainer}; fn dispatch(editor: &mut EditorTestUtils, message: impl Into) -> Vec { - editor.editor.handle_message(message) + editor.dispatch(message) } fn portfolio(editor: &EditorTestUtils) -> &PortfolioMessageHandler { diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index bed2d19c5a2..5be7f158b70 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -1221,12 +1221,8 @@ impl PortfolioMessageHandler { } pub fn poll_node_graph_evaluation(&mut self, responses: &mut VecDeque) -> Result<(), String> { - let Some(document_id) = self.active_document_id else { - return Err("No active document".to_string()); - }; - let Some(active_document) = self.documents.get_mut(&document_id) else { - return Err("No active document".to_string()); - }; + let Some(document_id) = self.active_document_id else { return Ok(()) }; + let Some(active_document) = self.documents.get_mut(&document_id) else { return Ok(()) }; let result = self.executor.poll_node_graph_evaluation(active_document, document_id, responses); if result.is_err() { diff --git a/editor/src/messages/prelude.rs b/editor/src/messages/prelude.rs index e183c1dfb90..8420a16ba3e 100644 --- a/editor/src/messages/prelude.rs +++ b/editor/src/messages/prelude.rs @@ -16,7 +16,7 @@ pub use crate::messages::dialog::new_document_dialog::{NewDocumentDialogMessage, pub use crate::messages::dialog::preferences_dialog::{PreferencesDialogMessage, PreferencesDialogMessageContext, PreferencesDialogMessageDiscriminant, PreferencesDialogMessageHandler}; pub use crate::messages::dialog::{DialogMessage, DialogMessageContext, DialogMessageDiscriminant, DialogMessageHandler}; pub use crate::messages::frontend::{FrontendMessage, FrontendMessageDiscriminant}; -pub use crate::messages::future::{FutureMessage, FutureMessageContext, FutureMessageDiscriminant, FutureMessageHandler, MessageFuture, Wake}; +pub use crate::messages::future::{FutureMessage, FutureMessageDiscriminant, FutureMessageHandler, MessageFuture, Wake}; pub use crate::messages::input_mapper::key_mapping::{KeyMappingMessage, KeyMappingMessageContext, KeyMappingMessageDiscriminant, KeyMappingMessageHandler}; pub use crate::messages::input_mapper::{InputMapperMessage, InputMapperMessageContext, InputMapperMessageDiscriminant, InputMapperMessageHandler}; pub use crate::messages::input_preprocessor::{InputPreprocessorMessage, InputPreprocessorMessageContext, InputPreprocessorMessageDiscriminant, InputPreprocessorMessageHandler}; diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index 4a6231627b9..a02e8f68b57 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -25,7 +25,7 @@ use std::sync::mpsc::{Receiver, Sender}; mod runtime; pub use runtime::*; -#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)] pub struct ExecutionRequest { execution_id: u64, render_config: RenderConfig, @@ -60,7 +60,6 @@ pub struct CompilationResponse { pub enum NodeGraphUpdate { ExecutionResponse(Box), CompilationResponse(CompilationResponse), - TaskCompleted, EyedropperPreview(Raster), } @@ -70,6 +69,7 @@ pub struct NodeGraphExecutor { receiver: Receiver, current_execution_id: u64, futures: VecDeque<(u64, ExecutionContext)>, + pending: Option<(u64, ExecutionContext)>, node_graph_hash: u64, /// Full path from the root document network to the node currently being inspected by the Data panel, or empty if nothing is selected. /// The last element is the inspect target itself; preceding elements identify the nested subnetwork the node lives in, @@ -109,10 +109,10 @@ struct GradientMigration { } impl NodeGraphExecutor { - pub(crate) fn new() -> (Self, NodeRuntime) { + pub(crate) fn new(editor_wake: Wake) -> (Self, NodeRuntime) { let (request_sender, request_receiver) = std::sync::mpsc::channel(); let (response_sender, response_receiver) = std::sync::mpsc::channel(); - let node_runtime = NodeRuntime::new(request_receiver, response_sender); + let node_runtime = NodeRuntime::new(request_receiver, response_sender, editor_wake); let node_executor = Self { futures: Default::default(), @@ -123,6 +123,7 @@ impl NodeGraphExecutor { previous_node_to_inspect: Vec::new(), gradient_migration: None, gradient_migration_attempted: HashSet::new(), + pending: None, }; (node_executor, node_runtime) } @@ -385,10 +386,19 @@ impl NodeGraphExecutor { } } - let Some((queued_execution_id, execution_context)) = self.futures.pop_front() else { - panic!("InvalidGenerationId") + let execution_context = if self.futures.front().is_some_and(|&(queued_execution_id, _)| queued_execution_id == execution_id) { + let (_, execution_context) = self.futures.pop_front().expect("the front was just checked"); + if execution_context.export_config.is_none() && execution_context.measure_fill.is_none() { + self.pending = Some((execution_id, execution_context.clone())); + } + execution_context + } else if let Some((rendered_execution_id, execution_context)) = &self.pending + && *rendered_execution_id == execution_id + { + execution_context.clone() + } else { + continue; }; - assert_eq!(queued_execution_id, execution_id, "Missmatch in execution id"); // TODO: Eventually remove this document upgrade code // Gradient-migration measurement runs only read back the fill's evaluated geometry; they never render to the artwork. @@ -442,15 +452,9 @@ impl NodeGraphExecutor { document.network_interface.update_vector_modify(vector_modify); match (node_graph_output, execution_context.export_config) { - // Special handling for exporting the artwork (Some(node_graph_output), Some(export_config)) => self.process_export(node_graph_output, export_config, document, responses)?, (Some(node_graph_output), None) => self.process_node_graph_output(node_graph_output, responses)?, - (None, Some(_)) => responses.add(DialogMessage::DisplayDialogError { - title: "Unable to export document".to_string(), - description: "Part of the document is still loading. Export again once it has finished.".to_string(), - }), - // The last frame stays until the async source completes and the graph renders again - (None, None) => {} + (None, _) => {} } responses.add(DeferMessage::TriggerGraphRun { execution_id, @@ -501,7 +505,6 @@ impl NodeGraphExecutor { node_graph_errors, }); } - NodeGraphUpdate::TaskCompleted => responses.add(PortfolioMessage::SubmitActiveGraphRender), NodeGraphUpdate::EyedropperPreview(raster) => { let (data, width, height) = raster.to_flat_u8(); responses.add(EyedropperToolMessage::PreviewImage { data, width, height }); diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index e5e279f0a68..5fd597da23e 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -26,6 +26,7 @@ use graphene_std::{Artboard, Context, Graphic, NodeError, Poll}; use interpreted_executor::dynamic_executor::{DynamicExecutor, ResolvedDocumentNodeTypesDelta}; use interpreted_executor::util::wrap_network_in_scope; use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc::{Receiver, Sender}; /// Persistent data between graph executions. It's updated via message passing from the editor thread with [`GraphRuntimeRequest`]`. @@ -38,6 +39,10 @@ pub struct NodeRuntime { executor: DynamicExecutor, receiver: Receiver, sender: Sender, + editor_wake: Wake, + dirty: Arc, + pending: Option, + pending_export: Option, editor_preferences: EditorPreferences, old_graph: Option, update_thumbnails: bool, @@ -96,11 +101,15 @@ pub struct ExportConfig { } impl NodeRuntime { - pub fn new(receiver: Receiver, sender: Sender) -> Self { + pub fn new(receiver: Receiver, sender: Sender, editor_wake: Wake) -> Self { Self { executor: DynamicExecutor::default(), receiver, sender, + editor_wake, + dirty: Default::default(), + pending: None, + pending_export: None, editor_preferences: EditorPreferences::default(), old_graph: None, resources: ResourceRegistry::default(), @@ -134,17 +143,23 @@ impl NodeRuntime { } pub async fn run(&mut self) -> Option { + let texture = self.handle_requests().await; + (self.editor_wake)(); + texture + } + + async fn handle_requests(&mut self) -> Option { let mut preferences = None; let mut graph = None; let mut eyedropper = None; let mut execution = None; - for request in self.receiver.try_iter() { + + for request in self.pending_export.is_none().then_some(&self.receiver).into_iter().flat_map(Receiver::try_iter) { match request { GraphRuntimeRequest::GraphUpdate(_) => graph = Some(request), GraphRuntimeRequest::ExecutionRequest(ref execution_request) => { if execution_request.render_config.for_eyedropper { eyedropper = Some(request); - continue; } @@ -161,6 +176,10 @@ impl NodeRuntime { } } + if self.dirty.swap(false, Ordering::Acquire) && execution.is_none() { + execution = self.pending_export.or(self.pending).map(GraphRuntimeRequest::ExecutionRequest); + } + // Eydropper should use the same time and pointer to not invalidate the cache if let Some(GraphRuntimeRequest::ExecutionRequest(eyedropper)) = &mut eyedropper && let Some(GraphRuntimeRequest::ExecutionRequest(execution)) = &execution @@ -206,7 +225,14 @@ impl NodeRuntime { .send(NodeGraphUpdate::CompilationResponse(CompilationResponse { result, node_graph_errors })) .expect("Failed to send response"); } - GraphRuntimeRequest::ExecutionRequest(ExecutionRequest { execution_id, mut render_config, .. }) => { + GraphRuntimeRequest::ExecutionRequest(request @ ExecutionRequest { execution_id, mut render_config, .. }) => { + if !render_config.for_export && !render_config.for_eyedropper { + self.pending = Some(request); + } + if render_config.for_export { + self.pending_export = None; + } + // We may want to render via the SVG pipeline even though raster was requested, if SVG Preview render mode is active or WebGPU/Vello is unavailable if render_config.export_format == ExportFormat::Raster && (render_config.render_mode == RenderMode::SvgPreview || self.editor_api.application_io.as_ref().unwrap().gpu_executor().is_none()) @@ -236,6 +262,16 @@ impl NodeRuntime { return None; } }; + + match (render_config.for_export, &result) { + (true, ExecutionResult::Pending) => { + self.pending_export = Some(request); + return None; + } + (true, _) => self.dirty.store(true, Ordering::Release), + _ => {} + } + let mut responses = VecDeque::new(); // TODO: Only process monitor nodes if the graph has changed, not when only the Footprint changes if !render_config.for_eyedropper { @@ -574,7 +610,7 @@ impl NodeRuntime { pub(crate) fn replace_task_executor(&mut self, spawner: Spawner, wake: Wake) { let spawn = NodeTaskExecutor { spawner, - sender: self.sender.clone(), + dirty: self.dirty.clone(), wake, }; self.async_runtime = Some(AsyncRuntime::new(Arc::new(spawn))); @@ -587,17 +623,17 @@ impl NodeRuntime { struct NodeTaskExecutor { spawner: Spawner, - sender: Sender, + dirty: Arc, wake: Wake, } impl Spawn for NodeTaskExecutor { fn spawn(&self, task: Task) { - let sender = self.sender.clone(); + let dirty = self.dirty.clone(); let wake = self.wake.clone(); self.spawner.spawn(Box::pin(async move { task.await; - let _ = sender.send(NodeGraphUpdate::TaskCompleted); + dirty.store(true, Ordering::Release); wake(); })); } diff --git a/editor/src/test_utils.rs b/editor/src/test_utils.rs index 956db79516e..ec79922858e 100644 --- a/editor/src/test_utils.rs +++ b/editor/src/test_utils.rs @@ -18,21 +18,25 @@ use graph_craft::application_io::resource::HashMapResourceStorage; use graphene_std::raster::color::Color; use graphene_std::uuid::NodeId; use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; /// A set of utility functions to make the writing of editor test more declarative pub struct EditorTestUtils { pub editor: Editor, pub runtime: NodeRuntime, + woken: Arc, } impl EditorTestUtils { pub fn create() -> Self { let _ = env_logger::builder().is_test(true).try_init(); - let (mut editor, runtime) = Builder::new() + let woken = Arc::new(AtomicBool::new(false)); + let wake = woken.clone(); + let (editor, runtime) = Builder::new() .with_editor( EditorBuilder::new() - .with_wake(|| {}) + .with_wake(move || wake.store(true, Ordering::Relaxed)) .with_environment(DEFAULT_ENVIRONMENT) .with_resource_storage(Arc::new(HashMapResourceStorage::new())) .with_document_store(Arc::new(MemoryStore::default())), @@ -41,14 +45,23 @@ impl EditorTestUtils { .with_spawner(BlockingSpawner) .with_random_seed(0) .build(); + let mut editor = Self { editor, runtime, woken }; - editor.handle_message(PortfolioMessage::Init); + editor.dispatch(PortfolioMessage::Init); // Tests run with `.gdd` storage on and the dual-write soak validation enabled, so storage round-trip drift fails loud. - editor.handle_message(PreferencesMessage::SaveAsGdd { enabled: true }); - editor.handle_message(PreferencesMessage::ValidateStorageRoundTrip { enabled: true }); + editor.dispatch(PreferencesMessage::SaveAsGdd { enabled: true }); + editor.dispatch(PreferencesMessage::ValidateStorageRoundTrip { enabled: true }); - Self { editor, runtime } + editor + } + + pub fn dispatch(&mut self, message: impl Into) -> Vec { + let mut responses = self.editor.handle_message(message); + while self.woken.swap(false, Ordering::Relaxed) { + responses.extend(self.editor.handle_message(Message::Wake)); + } + responses } pub fn eval_graph<'a>(&'a mut self) -> impl std::future::Future> + 'a { @@ -86,7 +99,7 @@ impl EditorTestUtils { } pub async fn handle_message(&mut self, message: impl Into) -> Vec { - let frontend_messages_from_msg = self.editor.handle_message(message); + let frontend_messages_from_msg = self.dispatch(message); // Required to process any buffered messages if let Err(e) = self.eval_graph().await { diff --git a/frontend/wrapper/src/helpers.rs b/frontend/wrapper/src/helpers.rs index c7958ef7ba2..0d2d2ada7ea 100644 --- a/frontend/wrapper/src/helpers.rs +++ b/frontend/wrapper/src/helpers.rs @@ -125,11 +125,11 @@ impl Spawn for WasmSpawner { } } -/// Web editor wake: queues a microtask that dispatches [`FutureMessage::Wake`]. +/// Web editor wake: queues a microtask that dispatches [`Message::Wake`]. #[cfg(all(not(feature = "native"), target_family = "wasm"))] pub(crate) fn wake_editor() { wasm_bindgen_futures::spawn_local(async { - wrapper(|wrapper| wrapper.dispatch(FutureMessage::Wake)); + wrapper(|wrapper| wrapper.dispatch(Message::Wake)); }); } From 87e769c3f281f979c310f046764b8cb2fc2994ef Mon Sep 17 00:00:00 2001 From: Timon Date: Fri, 16 Oct 2026 18:48:04 +0000 Subject: [PATCH 48/54] Add source generation tests --- .../libraries/core-types/src/runtime.rs | 15 ++++++++ .../nodes/gcore/src/context_modification.rs | 38 +++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/node-graph/libraries/core-types/src/runtime.rs b/node-graph/libraries/core-types/src/runtime.rs index 67bd16ba2bc..f82f7b68e3e 100644 --- a/node-graph/libraries/core-types/src/runtime.rs +++ b/node-graph/libraries/core-types/src/runtime.rs @@ -254,6 +254,21 @@ mod tests { assert_eq!(&*runtime.snapshot(), &[(SOURCE, 1)]); } + #[test] + fn retaining_sources_keeps_live_generations() { + let spawner = Arc::new(CollectSpawner::default()); + let runtime = AsyncRuntime::new(spawner.clone()); + runtime.retain_sources(&[SOURCE]); + runtime.spawn(SOURCE, Box::pin(YieldOnce(false))); + spawner.drain(); + + runtime.retain_sources(&[SOURCE, SourceId(9)]); + assert_eq!(&*runtime.snapshot(), &[(SOURCE, 1), (SourceId(9), 0)]); + + runtime.retain_sources(&[SourceId(9)]); + assert_eq!(&*runtime.snapshot(), &[(SourceId(9), 0)]); + } + #[test] fn an_inline_completion_and_a_dropped_source_bump_nothing() { let spawner = Arc::new(CollectSpawner::default()); diff --git a/node-graph/nodes/gcore/src/context_modification.rs b/node-graph/nodes/gcore/src/context_modification.rs index 959d0f4ae63..9e57eed2b9f 100644 --- a/node-graph/nodes/gcore/src/context_modification.rs +++ b/node-graph/nodes/gcore/src/context_modification.rs @@ -55,6 +55,7 @@ fn context_modification( #[cfg(test)] mod tests { use super::*; + use core_types::SourceId; use core_types::context::ContextFeatures; use core_types::graphene_hash::CacheHash; use core_types::transform::Footprint; @@ -120,4 +121,41 @@ mod tests { "Hash should be stable when keeping only footprint and varargs and their values are the same" ); } + + fn hash_of(context: OwnedContextImpl) -> u64 { + let mut hasher = DefaultHasher::new(); + context.cache_hash(&mut hasher); + hasher.finish() + } + + fn with_generations(generations: &[(u64, u64)]) -> std::sync::Arc { + let generations = generations.iter().map(|&(source, generation)| (SourceId(source), generation)).collect(); + std::sync::Arc::new(OwnedContextImpl::empty().with_generations(generations)) + } + + #[test] + fn a_modification_keys_only_on_its_retained_sources() { + let modification = ContextModification { + features: ContextFeatures::all(), + sources: [SourceId(1)].into(), + }; + let key = |generations: &[(u64, u64)]| hash_of(OwnedContextImpl::from_modification(with_generations(generations), &modification)); + let initial = key(&[(0, 1), (1, 3)]); + assert_eq!(initial, key(&[(0, 2), (1, 3)]), "a bump of an unretained source keeps the key"); + assert_ne!(initial, key(&[(0, 1), (1, 4)]), "a bump of a retained source changes the key"); + } + + #[test] + fn a_source_key_ignores_its_own_bumps() { + let key = |generations: &[(u64, u64)]| hash_of(OwnedContextImpl::from(with_generations(generations)).without_source(SourceId(7))); + let initial = key(&[(7, 1), (9, 5)]); + assert_eq!(initial, key(&[(7, 2), (9, 5)]), "its own bump keeps the key"); + assert_ne!(initial, key(&[(7, 1), (9, 6)]), "another source's bump changes the key"); + } + + #[test] + fn an_unfiltered_context_sees_every_bump() { + let key = |generations: &[(u64, u64)]| hash_of(OwnedContextImpl::from(with_generations(generations))); + assert_ne!(key(&[(0, 1)]), key(&[(0, 2)])); + } } From cf9b898c14da90a0505f9c906be5576f46d2949e Mon Sep 17 00:00:00 2001 From: Timon Date: Fri, 16 Oct 2026 18:48:04 +0000 Subject: [PATCH 49/54] Add test for node error origin --- .../src/dynamic_executor/test.rs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/node-graph/interpreted-executor/src/dynamic_executor/test.rs b/node-graph/interpreted-executor/src/dynamic_executor/test.rs index bf63017d8eb..adc40f81d60 100644 --- a/node-graph/interpreted-executor/src/dynamic_executor/test.rs +++ b/node-graph/interpreted-executor/src/dynamic_executor/test.rs @@ -973,3 +973,34 @@ fn an_error_after_a_fallback_keeps_both() { }; assert_eq!(executor.find_origin(NodeId(3), upstream), Some(NodeId(2))); } + +#[test] +fn a_node_error_names_the_node_that_raised_it() { + let value_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::Number(1.).into()), vec![NodeId(0)]); + let fail_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::Bool(true).into()), vec![NodeId(1)]); + let mut error_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(0), NodeId(1)]), vec![NodeId(2)]); + error_node.identifier = graphene_std::poll_debug::error::IDENTIFIER; + let mut count_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(2)]), vec![NodeId(3)]); + count_node.identifier = graphene_std::poll_debug::evaluation_count::IDENTIFIER; + let executor = DynamicExecutor::new(ProtoNetwork { + inputs: vec![], + output: NodeId(3), + nodes: vec![(NodeId(0), value_node), (NodeId(1), fail_node), (NodeId(2), error_node), (NodeId(3), count_node)], + }) + .expect("The debug nodes should resolve"); + + let context: Context = None; + let Ok(Poll::Error(error)) = (&executor).execute(context) else { + panic!("The failing node should stop the frame") + }; + let origin = executor.find_origin(executor.output(), &error); + assert_eq!(origin, Some(NodeId(2)), "The error should name the failing node, not the root that forwarded it"); + let paths: Vec<_> = executor + .tree() + .source_map() + .iter() + .filter(|(_, (node, _))| Some(*node) == origin) + .map(|(path, _)| path.to_vec()) + .collect(); + assert_eq!(paths, [vec![NodeId(2)]]); +} From 4771b5e0aefb044e6ae647c32de20790c99a7ab6 Mon Sep 17 00:00:00 2001 From: Timon Date: Sat, 17 Oct 2026 14:44:58 +0000 Subject: [PATCH 50/54] Add render status overlay --- .../src/messages/frontend/frontend_message.rs | 5 ++- editor/src/messages/frontend/utility_types.rs | 9 ++++ .../src/components/panels/Document.svelte | 44 +++++++++++++++++++ frontend/src/stores/document.ts | 12 ++++- 4 files changed, 68 insertions(+), 2 deletions(-) diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index 50336924286..a630911adba 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -1,5 +1,5 @@ use super::IconName; -use super::utility_types::{MouseCursorIcon, PersistedState}; +use super::utility_types::{MouseCursorIcon, PersistedState, RenderStatus}; use crate::messages::app_window::app_window_message_handler::AppWindowPlatform; use crate::messages::frontend::utility_types::{DocumentInfo, EyedropperPreviewImage, FileDialogOptions, FileFilter, FileType, RasterizedImage}; use crate::messages::input_mapper::utility_types::misc::ActionShortcut; @@ -226,6 +226,9 @@ pub enum FrontendMessage { UpdateDocumentArtwork { svg: String, }, + UpdateRenderStatus { + status: RenderStatus, + }, UpdateImageData { image_data: Vec, }, diff --git a/editor/src/messages/frontend/utility_types.rs b/editor/src/messages/frontend/utility_types.rs index 27e2791ac8b..37ad351ee27 100644 --- a/editor/src/messages/frontend/utility_types.rs +++ b/editor/src/messages/frontend/utility_types.rs @@ -42,6 +42,15 @@ pub enum MouseCursorIcon { Rotate, } +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +pub enum RenderStatus { + #[default] + Rendering, + Error, + Panic, +} + #[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(from_wasm_abi))] #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Serialize, serde::Deserialize)] pub enum FileType { diff --git a/frontend/src/components/panels/Document.svelte b/frontend/src/components/panels/Document.svelte index e80ed12fe0a..6ccc6bfd4f5 100644 --- a/frontend/src/components/panels/Document.svelte +++ b/frontend/src/components/panels/Document.svelte @@ -719,6 +719,27 @@ {#if isWeb} Initializing Renderer… {/if} + {#if $document.renderStatus === "Error"} + + +

+ The document cannot render in its current state.
+ Undo to go back, if available, or check for error details
+ by clicking the Node Graph button up at the top right. +

+
+
+ {:else if $document.renderStatus === "Panic"} + + +

+ The document crashed while being rendered in its current state.
+ The editor is now unstable! Undo your last action to restore the artwork,
+ then save your document and restart the editor before continuing work. +

+
+
+ {/if}
@@ -956,6 +977,29 @@ } } + .render-status-overlay { + position: absolute; + inset: 0; + align-items: center; + justify-content: center; + background: var(--color-2-mildblack); + pointer-events: none; + + .text-label { + padding: 14px 20px; + border-radius: 4px; + background: var(--color-warning-yellow); + color: var(--color-2-mildblack); + font-size: 18px; + line-height: 24px; + text-align: center; + } + + &.panic .text-label { + background: var(--color-error-red); + } + } + .text-input div { cursor: text; background: none; diff --git a/frontend/src/stores/document.ts b/frontend/src/stores/document.ts index 491a191d0f0..905c481bd54 100644 --- a/frontend/src/stores/document.ts +++ b/frontend/src/stores/document.ts @@ -3,7 +3,7 @@ import { writable } from "svelte/store"; import type { Writable } from "svelte/store"; import type { SubscriptionsRouter } from "/src/subscriptions-router"; import { patchLayout } from "/src/utility-functions/widgets"; -import type { Layout } from "/wrapper/pkg/graphite_wasm_wrapper"; +import type { Layout, RenderStatus } from "/wrapper/pkg/graphite_wasm_wrapper"; export type DocumentStore = ReturnType; @@ -15,6 +15,7 @@ type DocumentStoreState = { nodeGraphControlBarLayout: Layout; graphViewOverlayOpen: boolean; fadeArtwork: number; + renderStatus: RenderStatus; }; const initialState: DocumentStoreState = { toolOptionsLayout: [], @@ -24,6 +25,7 @@ const initialState: DocumentStoreState = { nodeGraphControlBarLayout: [], graphViewOverlayOpen: false, fadeArtwork: 100, + renderStatus: "Rendering", }; let subscriptionsRouter: SubscriptionsRouter | undefined = undefined; @@ -97,6 +99,13 @@ export function createDocumentStore(subscriptions: SubscriptionsRouter) { }); }); + subscriptions.subscribeFrontendMessage("UpdateRenderStatus", (data) => { + update((state) => { + state.renderStatus = data.status; + return state; + }); + }); + return { subscribe }; } @@ -106,6 +115,7 @@ export function destroyDocumentStore() { subscriptions.unsubscribeFrontendMessage("UpdateGraphFadeArtwork"); subscriptions.unsubscribeFrontendMessage("UpdateGraphViewOverlay"); + subscriptions.unsubscribeFrontendMessage("UpdateRenderStatus"); subscriptions.unsubscribeLayoutUpdate("ToolOptions"); subscriptions.unsubscribeLayoutUpdate("DocumentBar"); subscriptions.unsubscribeLayoutUpdate("ToolShelf"); From 11cff62675eb1f274aebe652a671fb23aaf825cb Mon Sep 17 00:00:00 2001 From: Timon Date: Sat, 17 Oct 2026 15:09:49 +0000 Subject: [PATCH 51/54] Replace error artwork with render status --- .../portfolio/portfolio_message_handler.rs | 15 +----- editor/src/node_graph_executor.rs | 47 ++++++++++++------- .../src/components/panels/Document.svelte | 2 +- frontend/wrapper/src/helpers.rs | 5 -- frontend/wrapper/src/lib.rs | 19 ++------ 5 files changed, 34 insertions(+), 54 deletions(-) diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 5be7f158b70..c7bdd5ff6b5 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -1224,20 +1224,7 @@ impl PortfolioMessageHandler { let Some(document_id) = self.active_document_id else { return Ok(()) }; let Some(active_document) = self.documents.get_mut(&document_id) else { return Ok(()) }; - let result = self.executor.poll_node_graph_evaluation(active_document, document_id, responses); - if result.is_err() { - let error = r#" - - - The document cannot render in its current state. - Undo to go back, if available, or check for error details - by clicking the Node Graph button up at the top right. - /text>"# - // It's a mystery why the `/text>` tag above needs to be missing its `<`, but when it exists it prints the `<` character in the text. However this works with it removed. - .to_string(); - responses.add(FrontendMessage::UpdateDocumentArtwork { svg: error }); - } - result + self.executor.poll_node_graph_evaluation(active_document, document_id, responses) } fn document_details(&self, document_id: DocumentId) -> Option { diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index a02e8f68b57..54d594365fc 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -1,4 +1,4 @@ -use crate::messages::frontend::utility_types::{ExportBounds, FileType, RasterizedImage}; +use crate::messages::frontend::utility_types::{ExportBounds, FileType, RasterizedImage, RenderStatus}; use crate::messages::portfolio::document::node_graph::utility_types::NodeEvaluationError; use crate::messages::portfolio::document::utility_types::network_interface::InputConnector; use crate::messages::prelude::*; @@ -359,7 +359,7 @@ impl NodeGraphExecutor { Ok(()) } - pub fn poll_node_graph_evaluation(&mut self, document: &mut DocumentMessageHandler, document_id: DocumentId, responses: &mut VecDeque) -> Result<(), String> { + pub fn poll_node_graph_evaluation(&mut self, document: &mut DocumentMessageHandler, document_id: DocumentId, responses: &mut VecDeque) { // TODO: Eventually remove this document upgrade code // A gradient migration belongs to one document; if the active document changed away from it, cancel so its stale in-flight response is ignored and revisiting restarts a fresh pass if self.gradient_migration.as_ref().is_some_and(|migration| migration.document_id != document_id) { @@ -444,7 +444,9 @@ impl NodeGraphExecutor { document.network_interface.update_click_targets(HashMap::new()); document.network_interface.update_outlines(HashMap::new()); document.network_interface.update_vector_modify(HashMap::new()); - return Err(format!("Node graph evaluation failed:\n{e}")); + log::error!("Node graph evaluation failed:\n{e}"); + responses.add(FrontendMessage::UpdateRenderStatus { status: RenderStatus::Error }); + return; } }; @@ -452,8 +454,8 @@ impl NodeGraphExecutor { document.network_interface.update_vector_modify(vector_modify); match (node_graph_output, execution_context.export_config) { - (Some(node_graph_output), Some(export_config)) => self.process_export(node_graph_output, export_config, document, responses)?, - (Some(node_graph_output), None) => self.process_node_graph_output(node_graph_output, responses)?, + (Some(node_graph_output), Some(export_config)) => self.process_export(node_graph_output, export_config, document, responses), + (Some(node_graph_output), None) => self.process_node_graph_output(node_graph_output, responses), (None, _) => {} } responses.add(DeferMessage::TriggerGraphRun { @@ -495,7 +497,8 @@ impl NodeGraphExecutor { node_graph_errors, }); - return Err(format!("Node graph evaluation failed:\n{e}")); + responses.add(FrontendMessage::UpdateRenderStatus { status: RenderStatus::Error }); + return; } Ok(result) => result, }; @@ -511,8 +514,6 @@ impl NodeGraphExecutor { } } } - - Ok(()) } // TODO: Eventually remove this document upgrade code @@ -698,9 +699,11 @@ impl NodeGraphExecutor { self.node_graph_hash = 0; } - fn process_node_graph_output(&mut self, node_graph_output: TaggedValue, responses: &mut VecDeque) -> Result<(), String> { + fn process_node_graph_output(&mut self, node_graph_output: TaggedValue, responses: &mut VecDeque) { let TaggedValue::RenderOutput(render_output) = node_graph_output else { - return Err(format!("Invalid node graph output type: {node_graph_output:#?}")); + log::error!("Invalid node graph output type: {node_graph_output:#?}"); + responses.add(FrontendMessage::UpdateRenderStatus { status: RenderStatus::Error }); + return; }; match render_output.data { @@ -728,7 +731,11 @@ impl NodeGraphExecutor { responses.add(FrontendMessage::UpdateDocumentArtwork { svg }); } RenderOutputType::Texture { .. } => {} - _ => return Err(format!("Invalid node graph output type: {:#?}", render_output.data)), + _ => { + log::error!("Invalid node graph output type: {:#?}", render_output.data); + responses.add(FrontendMessage::UpdateRenderStatus { status: RenderStatus::Error }); + return; + } } let RenderMetadata { @@ -760,10 +767,10 @@ impl NodeGraphExecutor { responses.add(DocumentMessage::RenderRulers); responses.add(OverlaysMessage::Draw); - Ok(()) + responses.add(FrontendMessage::UpdateRenderStatus { status: RenderStatus::Rendering }); } - fn process_export(&self, node_graph_output: TaggedValue, export_config: ExportConfig, document: &DocumentMessageHandler, responses: &mut VecDeque) -> Result<(), String> { + fn process_export(&self, node_graph_output: TaggedValue, export_config: ExportConfig, document: &DocumentMessageHandler, responses: &mut VecDeque) { let ExportConfig { file_type, name, @@ -803,15 +810,19 @@ impl NodeGraphExecutor { data: RenderOutputType::Buffer { data, width, height }, .. }) if file_type != FileType::Svg => { - let content = file_type.encode(width, height, data)?.into(); + let content = match file_type.encode(width, height, data) { + Ok(content) => content.into(), + Err(e) => { + log::error!("Failed to encode image: {e}"); + return; + } + }; responses.add(FrontendMessage::TriggerSaveFile { name, folder, filters, content }); } _ => { - return Err(format!("Incorrect render type for exporting to an SVG ({file_type:?}, {node_graph_output})")); + log::error!("Incorrect render type for exporting to an SVG ({file_type:?}, {node_graph_output})"); } - }; - - Ok(()) + } } } diff --git a/frontend/src/components/panels/Document.svelte b/frontend/src/components/panels/Document.svelte index 6ccc6bfd4f5..94b7e84946d 100644 --- a/frontend/src/components/panels/Document.svelte +++ b/frontend/src/components/panels/Document.svelte @@ -716,7 +716,7 @@ > {/if} - {#if isWeb} + {#if isWeb && $document.renderStatus === "Rendering"} Initializing Renderer… {/if} {#if $document.renderStatus === "Error"} diff --git a/frontend/wrapper/src/helpers.rs b/frontend/wrapper/src/helpers.rs index 0d2d2ada7ea..36cb0101d51 100644 --- a/frontend/wrapper/src/helpers.rs +++ b/frontend/wrapper/src/helpers.rs @@ -98,11 +98,6 @@ pub(crate) async fn poll_node_graph_evaluation() { } } - // Clear the error display if there are no more errors - if !messages.is_empty() { - crate::NODE_GRAPH_ERROR_DISPLAYED.store(false, Ordering::SeqCst); - } - // Batch responses to pool frontend updates let batched = Message::Batched { messages: messages.into_iter().collect(), diff --git a/frontend/wrapper/src/lib.rs b/frontend/wrapper/src/lib.rs index a38892db0ba..b953256c0df 100644 --- a/frontend/wrapper/src/lib.rs +++ b/frontend/wrapper/src/lib.rs @@ -16,6 +16,8 @@ pub use editor_commands::EditorCommand; #[cfg(feature = "editor")] use crate::helpers::wrapper; #[cfg(feature = "editor")] +use editor::messages::frontend::utility_types::RenderStatus; +#[cfg(feature = "editor")] use editor::messages::prelude::*; use std::panic; use std::sync::Mutex; @@ -25,7 +27,6 @@ use wasm_bindgen::prelude::*; // Set up the persistent editor backend state pub static EDITOR_HAS_CRASHED: AtomicBool = AtomicBool::new(false); pub static FRONTEND_READY: AtomicBool = AtomicBool::new(false); -pub static NODE_GRAPH_ERROR_DISPLAYED: AtomicBool = AtomicBool::new(false); pub static LOGGER: WasmLog = WasmLog; #[cfg(not(feature = "native"))] pub static NODE_RUNTIME: spin::Mutex> = spin::Mutex::new(None); @@ -67,21 +68,7 @@ pub fn panic_hook(info: &panic::PanicHookInfo) { unsafe { NODE_RUNTIME.force_unlock() }; } - if !NODE_GRAPH_ERROR_DISPLAYED.load(Ordering::SeqCst) { - NODE_GRAPH_ERROR_DISPLAYED.store(true, Ordering::SeqCst); - wrapper(|wrapper| { - let error = r#" - - - The document crashed while being rendered in its current state. - The editor is now unstable! Undo your last action to restore the artwork, - then save your document and restart the editor before continuing work. - /text>"# - // It's a mystery why the `/text>` tag above needs to be missing its `<`, but when it exists it prints the `<` character in the text. However this works with it removed. - .to_string(); - wrapper.send_frontend_message_to_js(FrontendMessage::UpdateDocumentArtwork { svg: error }); - }); - } + wrapper(|wrapper| wrapper.send_frontend_message_to_js(FrontendMessage::UpdateRenderStatus { status: RenderStatus::Panic })); return; } From e1ee8a4f1c35ef65017658c3aaad1c86c27d1f12 Mon Sep 17 00:00:00 2001 From: Timon Date: Sat, 17 Oct 2026 15:58:12 +0000 Subject: [PATCH 52/54] Poll node graph through portfolio message --- .../src/handle_desktop_wrapper_message.rs | 1 - desktop/wrapper/src/message_dispatcher.rs | 12 ---- desktop/wrapper/src/messages.rs | 1 - editor/src/application.rs | 4 -- editor/src/dispatcher.rs | 8 +-- .../document/document_message_handler.rs | 5 +- .../messages/portfolio/portfolio_message.rs | 1 + .../portfolio/portfolio_message_handler.rs | 13 ++-- editor/src/test_utils.rs | 8 +-- frontend/wrapper/src/editor_wrapper.rs | 6 +- frontend/wrapper/src/helpers.rs | 63 ++----------------- 11 files changed, 24 insertions(+), 98 deletions(-) diff --git a/desktop/wrapper/src/handle_desktop_wrapper_message.rs b/desktop/wrapper/src/handle_desktop_wrapper_message.rs index 072869017a9..5c4bbcc2577 100644 --- a/desktop/wrapper/src/handle_desktop_wrapper_message.rs +++ b/desktop/wrapper/src/handle_desktop_wrapper_message.rs @@ -35,7 +35,6 @@ pub(super) fn handle_desktop_wrapper_message(dispatcher: &mut DesktopWrapperMess dispatcher.respond(DesktopFrontendMessage::WriteFile { path, content }); } }, - DesktopWrapperMessage::PollNodeGraphEvaluation => dispatcher.poll_node_graph_evaluation(), DesktopWrapperMessage::UpdateMaximized { maximized } => { let message = FrontendMessage::UpdateMaximized { maximized }; dispatcher.queue_editor_message(message); diff --git a/desktop/wrapper/src/message_dispatcher.rs b/desktop/wrapper/src/message_dispatcher.rs index 11ac36c90f1..9f35fb3f2dc 100644 --- a/desktop/wrapper/src/message_dispatcher.rs +++ b/desktop/wrapper/src/message_dispatcher.rs @@ -42,18 +42,6 @@ impl<'a> DesktopWrapperMessageDispatcher<'a> { self.responses.push(response); } - pub(super) fn poll_node_graph_evaluation(&mut self) { - let mut responses = VecDeque::new(); - if let Err(e) = self.editor.poll_node_graph_evaluation(&mut responses) - && e != "No active document" - { - tracing::error!("Error polling node graph: {}", e); - } - while let Some(message) = responses.pop_front() { - self.queue_editor_message(message); - } - } - fn process_queue(&mut self) { let mut frontend_messages = Vec::new(); diff --git a/desktop/wrapper/src/messages.rs b/desktop/wrapper/src/messages.rs index b3663ea87eb..382e6622466 100644 --- a/desktop/wrapper/src/messages.rs +++ b/desktop/wrapper/src/messages.rs @@ -79,7 +79,6 @@ pub enum DesktopWrapperMessage { Input(InputMessage), IngestFile { path: PathBuf, content: Vec, action: IngestAction }, SaveFileDialogResult { path: PathBuf, context: SaveFileDialogContext }, - PollNodeGraphEvaluation, UpdateMaximized { maximized: bool }, UpdateFullscreen { fullscreen: bool }, LoadPersistedState { state: PersistedState }, diff --git a/editor/src/application.rs b/editor/src/application.rs index e005c67114c..3d60214569a 100644 --- a/editor/src/application.rs +++ b/editor/src/application.rs @@ -12,10 +12,6 @@ impl Editor { std::mem::take(&mut self.dispatcher.responses) } - - pub fn poll_node_graph_evaluation(&mut self, responses: &mut VecDeque) -> Result<(), String> { - self.dispatcher.poll_node_graph_evaluation(responses) - } } #[derive(Clone, Copy, Debug)] diff --git a/editor/src/dispatcher.rs b/editor/src/dispatcher.rs index 31228097599..6a9a1340147 100644 --- a/editor/src/dispatcher.rs +++ b/editor/src/dispatcher.rs @@ -362,9 +362,7 @@ impl Dispatcher { } // Messages Message::Wake => { - if let Err(error) = self.poll_node_graph_evaluation(&mut queue) { - log::error!("Error polling node graph: {error}"); - } + queue.add(PortfolioMessage::PollNodeGraphEvaluation); queue.add(FutureMessage::Drain); } Message::Batched { messages } => { @@ -401,10 +399,6 @@ impl Dispatcher { list } - pub fn poll_node_graph_evaluation(&mut self, responses: &mut VecDeque) -> Result<(), String> { - self.message_handlers.portfolio_message_handler.poll_node_graph_evaluation(responses) - } - /// Create the tree structure for logging the messages as a tree fn create_indents(queues: &[VecDeque]) -> String { String::from_iter(queues.iter().enumerate().skip(1).map(|(index, queue)| { diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index a0e6600b50a..63339f86230 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -4040,6 +4040,7 @@ fn deserialize_collapsed_layers<'de, D: serde::Deserializer<'de>>(deserializer: #[cfg(test)] mod document_message_handler_tests { use super::*; + use crate::messages::frontend::utility_types::RenderStatus; use crate::test_utils::test_prelude::*; #[test] @@ -4232,8 +4233,8 @@ mod document_message_handler_tests { .unwrap(); editor.runtime.run().await; - let mut messages = VecDeque::new(); - editor.editor.poll_node_graph_evaluation(&mut messages).unwrap(); + let frontend_messages = editor.editor.handle_message(Message::Wake); + assert!(!frontend_messages.contains(&FrontendMessage::UpdateRenderStatus { status: RenderStatus::Error })); } #[tokio::test] diff --git a/editor/src/messages/portfolio/portfolio_message.rs b/editor/src/messages/portfolio/portfolio_message.rs index 0f7cb3915cf..d65451beabd 100644 --- a/editor/src/messages/portfolio/portfolio_message.rs +++ b/editor/src/messages/portfolio/portfolio_message.rs @@ -149,6 +149,7 @@ pub enum PortfolioMessage { ignore_hash: bool, }, SubmitEyedropperPreviewRender, + PollNodeGraphEvaluation, ToggleResetNodesToDefinitionsOnOpen, ToggleRulers, UpdateDocumentWidgets, diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index c7bdd5ff6b5..98799ee207f 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -977,6 +977,12 @@ impl MessageHandler> for Portfolio Ok(message) => responses.add_front(message), } } + PortfolioMessage::PollNodeGraphEvaluation => { + let Some(document_id) = self.active_document_id else { return }; + let Some(active_document) = self.documents.get_mut(&document_id) else { return }; + + self.executor.poll_node_graph_evaluation(active_document, document_id, responses); + } PortfolioMessage::ToggleResetNodesToDefinitionsOnOpen => { self.reset_node_definitions_on_open = !self.reset_node_definitions_on_open; responses.add(MenuBarMessage::SendLayout); @@ -1220,13 +1226,6 @@ impl PortfolioMessageHandler { self.document_ids.iter().position(|id| id == &document_id).expect("Active document is missing from document ids") } - pub fn poll_node_graph_evaluation(&mut self, responses: &mut VecDeque) -> Result<(), String> { - let Some(document_id) = self.active_document_id else { return Ok(()) }; - let Some(active_document) = self.documents.get_mut(&document_id) else { return Ok(()) }; - - self.executor.poll_node_graph_evaluation(active_document, document_id, responses) - } - fn document_details(&self, document_id: DocumentId) -> Option { if let Some(document) = self.documents.get(&document_id) { Some(DocumentInfo { diff --git a/editor/src/test_utils.rs b/editor/src/test_utils.rs index ec79922858e..17b1656e3e1 100644 --- a/editor/src/test_utils.rs +++ b/editor/src/test_utils.rs @@ -1,5 +1,6 @@ use crate::application::{DEFAULT_ENVIRONMENT, Editor}; use crate::builder::{Builder, EditorBuilder, RuntimeBuilder}; +use crate::messages::frontend::utility_types::RenderStatus; use crate::messages::input_mapper::utility_types::keyboard::ModifierKeys; use crate::messages::input_mapper::utility_types::pointer::{EditorPointerState, MouseKeys, ViewportPosition}; use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier; @@ -82,11 +83,10 @@ impl EditorTestUtils { } 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}")); + let frontend_messages = editor.handle_message(Message::Wake); + if frontend_messages.contains(&FrontendMessage::UpdateRenderStatus { status: RenderStatus::Error }) { + return Err("Graph should render".to_string()); } - let frontend_messages = messages.into_iter().flat_map(|message| editor.handle_message(message)); for message in frontend_messages { message.check_node_graph_error(); diff --git a/frontend/wrapper/src/editor_wrapper.rs b/frontend/wrapper/src/editor_wrapper.rs index 3f2f28e7ae6..071fb9992c7 100644 --- a/frontend/wrapper/src/editor_wrapper.rs +++ b/frontend/wrapper/src/editor_wrapper.rs @@ -8,7 +8,7 @@ use crate::MESSAGE_BUFFER; #[cfg(all(feature = "native", target_family = "wasm"))] use crate::editor_commands::EditorCommand; #[cfg(not(feature = "native"))] -use crate::helpers::poll_node_graph_evaluation; +use crate::helpers::run_node_runtime; #[cfg(any(not(feature = "native"), target_family = "wasm"))] use crate::helpers::wrapper; #[cfg(feature = "editor")] @@ -189,14 +189,14 @@ impl EditorWrapper { #[cfg(target_family = "wasm")] self.init_portfolio(); - // Poll node graph evaluation on `requestAnimationFrame` + // Run the node runtime on `requestAnimationFrame` { let f = std::rc::Rc::new(RefCell::new(None)); let g = f.clone(); *g.borrow_mut() = Some(Closure::new(move |_timestamp| { #[cfg(not(feature = "native"))] - wasm_bindgen_futures::spawn_local(poll_node_graph_evaluation()); + wasm_bindgen_futures::spawn_local(run_node_runtime()); #[cfg(any(not(feature = "native"), target_family = "wasm"))] wrapper(|wrapper| { diff --git a/frontend/wrapper/src/helpers.rs b/frontend/wrapper/src/helpers.rs index 36cb0101d51..10bd4bf9c95 100644 --- a/frontend/wrapper/src/helpers.rs +++ b/frontend/wrapper/src/helpers.rs @@ -1,15 +1,11 @@ #[cfg(not(feature = "native"))] -use crate::EDITOR; -#[cfg(not(feature = "native"))] use crate::EDITOR_HAS_CRASHED; use crate::EDITOR_WRAPPER; use crate::editor_wrapper::EditorWrapper; use crate::native_communication::RasterizedImage; -#[cfg(not(feature = "native"))] -use editor::application::Editor; #[cfg(feature = "editor")] use editor::messages::input_mapper::utility_types::keyboard::Key; -#[cfg(not(feature = "native"))] +#[cfg(all(not(feature = "native"), target_family = "wasm"))] use editor::messages::prelude::*; #[cfg(all(not(feature = "native"), target_family = "wasm"))] use editor::task::{Spawn, Task}; @@ -38,30 +34,6 @@ pub(crate) fn set_timeout(f: &Closure, delay: Duration) { .expect("Failed to call `setTimeout`"); } -/// Provides access to the `Editor` by calling the given closure with it as an argument. -#[cfg(not(feature = "native"))] -fn editor(callback: impl FnOnce(&mut editor::application::Editor) -> T) -> T { - EDITOR.with(|editor| { - let mut guard = editor.try_lock(); - let Ok(Some(editor)) = guard.as_deref_mut() else { - log::error!("Failed to borrow editor"); - return T::default(); - }; - - callback(editor) - }) -} - -/// Provides access to the `Editor` and its `EditorWrapper` by calling the given closure with them as arguments. -#[cfg(not(feature = "native"))] -pub(crate) fn editor_and_wrapper(callback: impl FnOnce(&mut Editor, &mut EditorWrapper)) { - wrapper(|wrapper| { - editor(|editor| { - // Call the closure with the editor and its wrapper - callback(editor, wrapper); - }) - }); -} /// Provides access to the `EditorWrapper` by calling the given closure with them as arguments. pub(crate) fn wrapper(callback: impl FnOnce(&mut EditorWrapper)) { EDITOR_WRAPPER.with(|wrapper| { @@ -77,38 +49,15 @@ pub(crate) fn wrapper(callback: impl FnOnce(&mut EditorWrapper)) { } #[cfg(not(feature = "native"))] -pub(crate) async fn poll_node_graph_evaluation() { +pub(crate) async fn run_node_runtime() { // Process no further messages after a crash to avoid spamming the console if EDITOR_HAS_CRASHED.load(Ordering::SeqCst) { return; } - { - let Some(mut runtime) = crate::NODE_RUNTIME.try_lock() else { return }; - let Some(runtime) = runtime.as_mut() else { return }; - runtime.run().await; - } - - editor_and_wrapper(|editor, wrapper| { - let mut messages = VecDeque::new(); - if let Err(e) = editor.poll_node_graph_evaluation(&mut messages) { - // TODO: This is a hacky way to suppress the error, but it shouldn't be generated in the first place - if e != "No active document" { - error!("Error evaluating node graph:\n{e}"); - } - } - - // Batch responses to pool frontend updates - let batched = Message::Batched { - messages: messages.into_iter().collect(), - }; - // Send each `FrontendMessage` to the JavaScript frontend - for response in editor.handle_message(batched) { - wrapper.send_frontend_message_to_js(response); - } - - // If the editor cannot be borrowed then it has encountered a panic - we should just ignore new dispatches - }); + let Some(mut runtime) = crate::NODE_RUNTIME.try_lock() else { return }; + let Some(runtime) = runtime.as_mut() else { return }; + runtime.run().await; } #[cfg(all(not(feature = "native"), target_family = "wasm"))] @@ -131,7 +80,7 @@ pub(crate) fn wake_editor() { /// Web runtime wake: queues a run of the node runtime. Used to wake the runtime on async task completion. #[cfg(all(not(feature = "native"), target_family = "wasm"))] pub(crate) fn wake_runtime() { - wasm_bindgen_futures::spawn_local(poll_node_graph_evaluation()); + wasm_bindgen_futures::spawn_local(run_node_runtime()); } /// Blits each rasterized image to a canvas registered on `window.imageCanvases` From f21e3508c807fc188986a2fbf2ce8d052ee22fd5 Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Sat, 17 Oct 2026 19:19:53 +0200 Subject: [PATCH 53/54] Minor review fixups --- node-graph/libraries/core-types/src/poll.rs | 8 ++++++++ node-graph/libraries/core-types/src/runtime.rs | 6 +++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/node-graph/libraries/core-types/src/poll.rs b/node-graph/libraries/core-types/src/poll.rs index 8567c2947e3..6a9889afd07 100644 --- a/node-graph/libraries/core-types/src/poll.rs +++ b/node-graph/libraries/core-types/src/poll.rs @@ -1,3 +1,5 @@ +#![allow(clippy::inline_always)] + use crate::Node; use std::cell::Cell; use std::error::Error; @@ -75,6 +77,7 @@ impl NodeError { } } + #[must_use] pub fn with_upstream(mut self, upstream: Vec) -> Self { self.upstream = upstream; self @@ -84,14 +87,17 @@ impl NodeError { self.trace.push(input); } + #[must_use] pub fn trace(&self) -> &[usize] { &self.trace } + #[must_use] pub fn upstream(&self) -> &[NodeError] { &self.upstream } + #[must_use] pub fn error(&self) -> &Arc { &self.error } @@ -140,10 +146,12 @@ pub struct Status { } impl Status { + #[must_use] pub fn new() -> Self { Self::default() } + #[must_use] pub fn no_partial() -> Self { Self { no_partial: true, ..Self::default() } } diff --git a/node-graph/libraries/core-types/src/runtime.rs b/node-graph/libraries/core-types/src/runtime.rs index f82f7b68e3e..346cbabfdb8 100644 --- a/node-graph/libraries/core-types/src/runtime.rs +++ b/node-graph/libraries/core-types/src/runtime.rs @@ -22,6 +22,7 @@ pub trait Spawn { pub struct SourceId(pub u64); impl SourceId { + #[must_use] pub fn from_path(path: &[NodeId]) -> Self { let mut hasher = rustc_hash::FxHasher::default(); path.hash(&mut hasher); @@ -57,7 +58,9 @@ impl PartialEq for RuntimeHandle { } impl graphene_hash::CacheHash for RuntimeHandle { - fn cache_hash(&self, _state: &mut H) {} + fn cache_hash(&self, state: &mut H) { + state.write_usize(Arc::as_ptr(&self.0) as usize); + } } pub struct AsyncRuntime(Arc); @@ -85,6 +88,7 @@ impl AsyncRuntime { snapshot.into() } + #[must_use] pub fn handle(&self) -> RuntimeHandle { RuntimeHandle(Arc::clone(&self.0)) } From b03061a27e763fd31abdbc8ce208d2745fec818f Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Sat, 17 Oct 2026 18:22:03 +0200 Subject: [PATCH 54/54] Remove future type and macro --- .../document/node_graph/node_properties.rs | 5 +-- node-graph/graph-craft/src/document/value.rs | 2 - node-graph/graph-craft/src/proto.rs | 4 +- .../interpreted-executor/src/node_registry.rs | 30 ++++++------- node-graph/libraries/core-types/src/lib.rs | 7 --- node-graph/libraries/core-types/src/types.rs | 43 ------------------- node-graph/node-macro/src/codegen.rs | 8 ++-- node-graph/preprocessor/src/lib.rs | 2 +- 8 files changed, 23 insertions(+), 78 deletions(-) diff --git a/editor/src/messages/portfolio/document/node_graph/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs index ca0876d2b63..f2892db153e 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -390,7 +390,6 @@ pub(crate) fn property_from_type(node_id: NodeId, index: usize, ty: &Type, optio }, Type::Generic(_) => vec![TextLabel::new("Generic Type (Not Supported)").widget_instance()].into(), Type::Fn(_, out) => return property_from_type(node_id, index, out, options, context), - Type::Future(out) => return property_from_type(node_id, index, out, options, context), }; extra_widgets.push(widgets); @@ -3756,8 +3755,8 @@ mod tests { #[test] fn wire_element_reads_every_spelling_down_to_the_element() { // A stored value's ranked type, a registry row's fn type, and a mapped row's list all carry the same element - let row = Type::Fn(Box::new(concrete!(())), Box::new(Type::Future(Box::new(item!(f64))))); - let mapped_row = Type::Fn(Box::new(concrete!(())), Box::new(Type::Future(Box::new(list!(f64))))); + let row = Type::Fn(Box::new(concrete!(())), Box::new(item!(f64))); + let mapped_row = Type::Fn(Box::new(concrete!(())), Box::new(list!(f64))); for ty in [TaggedValue::Number(0.).ty(), item!(f64), list!(f64), row, mapped_row] { assert_eq!(wire_element(&ty), concrete!(f64), "{ty:?} should read down to its element"); } diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 01c9ba9b1ab..1c225b8e5e2 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -437,7 +437,6 @@ macro_rules! tagged_value { None } Type::Fn(_, output) => TaggedValue::from_type(output), - Type::Future(output) => TaggedValue::from_type(output), // Element types with a dedicated variant use it directly (the variant's value is a rank-0 cell); the rest store the structural type Type::Item(element) => TaggedValue::from_type(element).or_else(|| { macro_rules! check { @@ -727,7 +726,6 @@ impl TaggedValue { Some(ty) } Type::Fn(_, output) => TaggedValue::from_primitive_string(string, output), - Type::Future(fut) => TaggedValue::from_primitive_string(string, fut), Type::Item(element) => TaggedValue::from_primitive_string(string, element), Type::List(element) => TaggedValue::from_primitive_string(string, element), } diff --git a/node-graph/graph-craft/src/proto.rs b/node-graph/graph-craft/src/proto.rs index 5181e8fcbd2..67cd011d62f 100644 --- a/node-graph/graph-craft/src/proto.rs +++ b/node-graph/graph-craft/src/proto.rs @@ -738,7 +738,7 @@ impl TypingContext { // If the node has a value input we can infer the return type from it ConstructionArgs::Value(ref v) => { // TODO: This should return a reference to the value - let types = NodeIOTypes::new(concrete!(Context), Type::Future(Box::new(v.ty())), vec![]); + let types = NodeIOTypes::new(concrete!(Context), v.ty(), vec![]); self.inferred.insert(node_id, types.clone()); return Ok(types); } @@ -772,8 +772,6 @@ impl TypingContext { match (from, to) { // Direct comparison of two concrete types. (Type::Concrete(type1), Type::Concrete(type2)) => type1 == type2, - // Check inner type for futures - (Type::Future(type1), Type::Future(type2)) => valid_type(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. diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index 2731e89468e..1557726533e 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -27,7 +27,7 @@ use graphene_std::vector::misc::{ }; use graphene_std::vector::style::{DashPattern, GradientForm, GradientHueDirection, GradientInterpolation, GradientSpace, GradientSpread, StrokeAlign, StrokeCap, StrokeJoin}; use graphene_std::vector::{QRCodeErrorCorrectionLevel, Vector, VectorModification}; -use graphene_std::{Artboard, Context, Graphic, NodeIO, NodeIOTypes, ProtoNodeIdentifier, concrete, fn_type_fut, future}; +use graphene_std::{Artboard, Context, Graphic, NodeIO, NodeIOTypes, ProtoNodeIdentifier, concrete, fn_type}; use node_registry_macros::async_node; use std::collections::HashMap; use std::sync::Arc; @@ -169,8 +169,8 @@ fn node_registry() -> HashMap::new()); - let params = vec![fn_type_fut!(Context, $ty)]; - let node_io = NodeIO::::to_async_node_io(&node, params); + let params = vec![fn_type!(Context, $ty)]; + let node_io = NodeIO::::to_node_io(&node, params); node_io }, ) @@ -191,8 +191,8 @@ fn node_registry() -> HashMap::new(), graphene_std::value::ClonedNode::new(std::marker::PhantomData::<$to>), ); - let params = vec![fn_type_fut!(Context, $from)]; - let node_io = NodeIO::::to_async_node_io(&node, params); + let params = vec![fn_type!(Context, $from)]; + let node_io = NodeIO::::to_node_io(&node, params); node_io }, ) @@ -224,8 +224,8 @@ fn node_registry() -> HashMap::new(), graphene_std::value::ClonedNode::new(std::marker::PhantomData::<$element>), ); - let params = vec![fn_type_fut!(Context, $from)]; - let node_io = NodeIO::::to_async_node_io(&node, params); + let params = vec![fn_type!(Context, $from)]; + let node_io = NodeIO::::to_node_io(&node, params); node_io }, ) @@ -243,8 +243,8 @@ fn node_registry() -> HashMap>::new()); - let params = vec![fn_type_fut!(Context, Item<$element>)]; - let node_io = NodeIO::::to_async_node_io(&node, params); + let params = vec![fn_type!(Context, Item<$element>)]; + let node_io = NodeIO::::to_node_io(&node, params); node_io }, ) @@ -262,8 +262,8 @@ fn node_registry() -> HashMap>::new()); - let params = vec![fn_type_fut!(Context, List<$element>)]; - let node_io = NodeIO::::to_async_node_io(&node, params); + let params = vec![fn_type!(Context, List<$element>)]; + let node_io = NodeIO::::to_node_io(&node, params); node_io }, ) @@ -281,8 +281,8 @@ fn node_registry() -> HashMap>>::new()); - let params = vec![fn_type_fut!(Context, Item>)]; - let node_io = NodeIO::::to_async_node_io(&node, params); + let params = vec![fn_type!(Context, Item>)]; + let node_io = NodeIO::::to_node_io(&node, params); node_io }, ) @@ -638,8 +638,8 @@ mod node_registry_macros { let node = <$path>::new($( graphene_std::any::PanicNode::<$arg, $type>::new() ),*); - let params = vec![$(fn_type_fut!($arg, $type)),*]; - let mut node_io = NodeIO::<$input>::to_async_node_io(&node, params); + let params = vec![$(fn_type!($arg, $type)),*]; + let mut node_io = NodeIO::<$input>::to_node_io(&node, params); node_io.call_argument = concrete!(<$input as StaticType>::Static); node_io }, diff --git a/node-graph/libraries/core-types/src/lib.rs b/node-graph/libraries/core-types/src/lib.rs index 00c016d900b..60fce461052 100644 --- a/node-graph/libraries/core-types/src/lib.rs +++ b/node-graph/libraries/core-types/src/lib.rs @@ -92,13 +92,6 @@ where inputs, } } - fn to_async_node_io(&self, inputs: Vec) -> NodeIOTypes { - NodeIOTypes { - call_argument: concrete!(::Static), - return_value: future!(::Static), - inputs, - } - } } impl, I> NodeIO for N diff --git a/node-graph/libraries/core-types/src/types.rs b/node-graph/libraries/core-types/src/types.rs index bccf8c641a3..2c275841246 100644 --- a/node-graph/libraries/core-types/src/types.rs +++ b/node-graph/libraries/core-types/src/types.rs @@ -60,19 +60,6 @@ macro_rules! list { }; } -// The `List<...>`/`Item<...>` rules must appear before the generic `$type:ty` rules, and in each macro that sees the literal tokens, -// because a type captured as `ty` becomes opaque to any inner macro's ranked pattern -#[macro_export] -macro_rules! future { - (List<$inner:ty>) => { - $crate::Type::Future(Box::new($crate::list!($inner))) - }; - (Item<$inner:ty>) => { - $crate::Type::Future(Box::new($crate::item!($inner))) - }; - ($type:ty) => {{ $crate::Type::Future(Box::new(concrete!($type))) }}; -} - #[macro_export] macro_rules! fn_type { (List<$inner:ty>) => { @@ -94,27 +81,6 @@ macro_rules! fn_type { $crate::Type::Fn(Box::new(concrete!($in_type)), Box::new(concrete!($type))) }; } -#[macro_export] -macro_rules! fn_type_fut { - (List<$inner:ty>) => { - $crate::Type::Fn(Box::new(concrete!(())), Box::new($crate::Type::Future(Box::new($crate::list!($inner))))) - }; - (Item<$inner:ty>) => { - $crate::Type::Fn(Box::new(concrete!(())), Box::new($crate::Type::Future(Box::new($crate::item!($inner))))) - }; - ($type:ty) => { - $crate::Type::Fn(Box::new(concrete!(())), Box::new(future!($type))) - }; - ($in_type:ty, List<$inner:ty>) => { - $crate::Type::Fn(Box::new(concrete!($in_type)), Box::new($crate::Type::Future(Box::new($crate::list!($inner))))) - }; - ($in_type:ty, Item<$inner:ty>) => { - $crate::Type::Fn(Box::new(concrete!($in_type)), Box::new($crate::Type::Future(Box::new($crate::item!($inner))))) - }; - ($in_type:ty, $type:ty) => { - $crate::Type::Fn(Box::new(concrete!($in_type)), Box::new(future!($type))) - }; -} // TODO: Rename to NodeSignatureMonomorphization #[derive(Clone, PartialEq, Eq, Hash, graphene_hash::CacheHash, Default)] @@ -269,8 +235,6 @@ pub enum Type { Concrete(TypeDescriptor), /// Runtime type information for a function. Given some input, gives some output. Fn(Box, Box), - /// Represents a future which promises to return the inner type. - Future(Box), /// Represents a recursive [Type] allowing nested levels of types to represent the type of an Item. Item(Box), /// Represents a list of this recursive [Type] allowing nested levels of types to represent the type of a List. @@ -346,7 +310,6 @@ impl Type { Self::Generic(_) => None, Self::Concrete(ty) => Some(ty.size), Self::Fn(_, _) => None, - Self::Future(_) => None, Self::Item(_) => None, Self::List(_) => None, } @@ -357,7 +320,6 @@ impl Type { Self::Generic(_) => None, Self::Concrete(ty) => Some(ty.align), Self::Fn(_, _) => None, - Self::Future(_) => None, Self::Item(_) => None, Self::List(_) => None, } @@ -368,7 +330,6 @@ impl Type { Self::Generic(_) => self, Self::Concrete(_) => self, Self::Fn(_, output) => output.nested_type(), - Self::Future(output) => output.nested_type(), Self::Item(_) => self, Self::List(_) => self, } @@ -382,7 +343,6 @@ impl Type { Self::Generic(_) => None, Self::Concrete(_) => None, Self::Fn(_, output) => output.replace_nested(f), - Self::Future(output) => output.replace_nested(f), Self::Item(_) => None, Self::List(_) => None, } @@ -393,7 +353,6 @@ impl Type { Type::Generic(name) => name.to_string(), Type::Concrete(ty) => simplify_identifier_name(&ty.name), Type::Fn(call_arg, return_value) => format!("{} called with {}", return_value.identifier_name(), call_arg.identifier_name()), - Type::Future(ty) => ty.identifier_name(), Type::Item(element) => element.identifier_name(), Type::List(element) => format!("{}[]", element.identifier_name()), } @@ -445,7 +404,6 @@ impl Type { Type::Concrete(descriptor) } Type::Fn(input, output) => Type::Fn(Box::new(input.normalize_rank()), Box::new(output.normalize_rank())), - Type::Future(inner) => Type::Future(Box::new(inner.normalize_rank())), Type::Item(element) => Type::Item(Box::new(element.normalize_rank())), Type::List(element) => Type::List(Box::new(element.normalize_rank())), Type::Generic(_) => self, @@ -566,7 +524,6 @@ impl std::fmt::Display for Type { Type::Generic(name) => write!(f, "{}", make_type_user_readable(name)), Type::Concrete(ty) => write!(f, "{ty}"), Type::Fn(_, return_value) => write!(f, "{return_value}"), - Type::Future(ty) => write!(f, "{ty}"), Type::Item(element) => write!(f, "{element}"), Type::List(element) => write!(f, "{element}[]"), } diff --git a/node-graph/node-macro/src/codegen.rs b/node-graph/node-macro/src/codegen.rs index 34286f2e30b..1d642a4f792 100644 --- a/node-graph/node-macro/src/codegen.rs +++ b/node-graph/node-macro/src/codegen.rs @@ -989,7 +989,7 @@ 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::{Node, NodeIOTypes, concrete, fn_type, ProtoNodeIdentifier, WasmNotSync, NodeIO, ContextFeature}; use gcore::value::ClonedNode; use gcore::ops::TypeNode; use gcore::registry::{NodeMetadata, FieldMetadata, NODE_REGISTRY, NODE_METADATA, DynAnyNode, DowncastBothNode, TypeErasedBox, PanicNode, RegistryValueSource, RegistryWidgetOverride}; @@ -1323,7 +1323,7 @@ fn generate_register_node_impl( let field_is_ranked = !regular_fields[j].is_environment() && matches!(®ular_fields[j].ty, ParsedFieldType::Item { .. }); let is_lazy_primary = Some(j) == primary_regular_index && matches!(regular_fields[j].ty, ParsedFieldType::Node { .. }); // The list-content variant lifts its lazy primary connector's `Item` content to `List`; ranked params follow the variant's param wrap. - // A ranked signature is spelled with bare `Item<...>`/`List<...>` tokens, which `fn_type_fut!` matches syntactically to construct the structural form. + // A ranked signature is spelled with bare `Item<...>`/`List<...>` tokens, which `fn_type!` matches syntactically to construct the structural form. let (output_type, signature_type) = if is_lazy_primary && variant == RegisterVariant::ListContent { let element_ty = peel_item(output_type).unwrap_or_else(|| output_type.clone()); (WireWrapper::List.apply(&gcore, &element_ty), Some(quote!(List<#element_ty>))) @@ -1348,7 +1348,7 @@ fn generate_register_node_impl( let #field_name: DowncastBothNode<#input_type, #output_type> = DowncastBothNode::new(args[#j].clone()); ); temp_constructors.push(downcast_node); - temp_node_io.push(quote!(fn_type_fut!(#input_type, #signature_type))); + temp_node_io.push(quote!(fn_type!(#input_type, #signature_type))); panic_node_types.push(quote!(#input_type, #output_type)); } let input_type = match parsed.input.implementations.is_empty() { @@ -1370,7 +1370,7 @@ fn generate_register_node_impl( }, { let node = #variant_struct_name::new(#(PanicNode::<#panic_node_types>::new(),)*); let params = vec![#(#temp_node_io,)*]; - NodeIO::<#input_type>::to_async_node_io(&node, params) + NodeIO::<#input_type>::to_node_io(&node, params) } ) )); diff --git a/node-graph/preprocessor/src/lib.rs b/node-graph/preprocessor/src/lib.rs index 1f314b1e761..151812b5294 100644 --- a/node-graph/preprocessor/src/lib.rs +++ b/node-graph/preprocessor/src/lib.rs @@ -344,7 +344,7 @@ pub fn node_inputs(fields: &[registry::FieldMetadata], first_node_io: &NodeIOTyp return NodeInput::value(TaggedValue::None, true); }; let ty = ty.clone().normalize_rank(); - let exposed = if index == 0 { ty != fn_type_fut!(Context, ()) } else { field.exposed }; + let exposed = if index == 0 { ty != fn_type!(Context, ()) } else { field.exposed }; match &field.value_source { RegistryValueSource::None => {}