diff --git a/Cargo.lock b/Cargo.lock index 28be754b338..f75eda47ec3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -913,6 +913,7 @@ dependencies = [ "color", "ctor", "dyn-any", + "futures", "glam 0.33.10", "graphene-hash", "image", @@ -932,7 +933,6 @@ dependencies = [ "serde_json", "skrifa 0.42.1", "tinyvec", - "tokio", "tsify", "wasm-bindgen", ] @@ -2132,7 +2132,6 @@ dependencies = [ "document-container", "document-format", "fern", - "futures", "graph-craft", "graphene-std", "image", @@ -2424,7 +2423,6 @@ dependencies = [ "serde_json", "simplecss", "snapping", - "spin", "thiserror 2.0.18", "tokio", "tsify", @@ -2432,7 +2430,6 @@ dependencies = [ "usvg", "vello", "wasm-bindgen", - "wasm-bindgen-futures", "web-sys", "wgpu-executor", "zip", @@ -2464,6 +2461,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..6b0ea29583e 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,15 @@ 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) => { + 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/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/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/handle_desktop_wrapper_message.rs b/desktop/wrapper/src/handle_desktop_wrapper_message.rs index df3237623af..5c4bbcc2577 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)); @@ -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/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/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/document/graph-storage/src/attributes/attr.rs b/document/graph-storage/src/attributes/attr.rs index 281d916d672..edf58496dde 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 d9593738bf9..1f3f54fc1aa 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; @@ -447,9 +446,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))?; @@ -477,11 +473,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 be511b4c3b7..3d2540082bb 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 00fbfb6a7ae..b4f121232ce 100644 --- a/document/graph-storage/src/to_runtime.rs +++ b/document/graph-storage/src/to_runtime.rs @@ -334,8 +334,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/Cargo.toml b/editor/Cargo.toml index 4806931fc97..ef03f79c114 100644 --- a/editor/Cargo.toml +++ b/editor/Cargo.toml @@ -53,7 +53,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 } @@ -69,9 +68,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 cd2f2c3f418..3d60214569a 100644 --- a/editor/src/application.rs +++ b/editor/src/application.rs @@ -1,76 +1,17 @@ use crate::dispatcher::Dispatcher; use crate::messages::prelude::*; -use graph_craft::application_io::PlatformApplicationIo; -use graph_craft::application_io::resource::ResourceStorage; pub use graphene_std::uuid::*; -use std::sync::{Arc, OnceLock}; 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 { - ENVIRONMENT.set(environment).expect("Editor shoud only be initialized once"); - graphene_std::uuid::set_uuid_seed(uuid_random_seed); - - let mut dispatcher = Dispatcher::new(resource_storage, document_store); - dispatcher.message_handlers.future_message_handler.set_wake(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 } - } - - #[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(); - let editor = Self { - dispatcher: Dispatcher::with_executor(executor), - }; - - 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); - - (editor, runtime) - } - pub fn handle_message>(&mut self, message: T) -> Vec { self.dispatcher.handle_message(message, true); 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) - } -} - -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)] @@ -78,6 +19,11 @@ 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, @@ -107,6 +53,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/builder.rs b/editor/src/builder.rs new file mode 100644 index 00000000000..f3bb6fe513b --- /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(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); + + 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 9a257a9d6d8..3df013f05a5 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; @@ -7,18 +8,20 @@ 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; -#[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, @@ -42,17 +45,27 @@ pub struct DispatcherMessageHandlers { } impl DispatcherMessageHandlers { - pub fn with_resource_storage(resource_storage: Arc) -> Self { + fn new(environment: Environment, executor: NodeGraphExecutor, document_store: Arc, future_message_handler: FutureMessageHandler) -> 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, + 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: PortfolioMessageHandler::new(executor, document_store), + preferences_message_handler: Default::default(), + resource_storage_message_handler: Default::default(), + tool_message_handler: Default::default(), + viewport_message_handler: Default::default(), } } } @@ -102,20 +115,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(resource_storage: Arc, document_store: Arc) -> Self { - let mut s = Self::default(); + 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, 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( - 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 } @@ -144,13 +158,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]); @@ -201,7 +208,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) => { @@ -237,6 +244,7 @@ impl Dispatcher { message, &mut queue, InputPreprocessorMessageContext { + environment: self.environment, viewport: &self.message_handlers.viewport_message_handler, }, ); @@ -245,13 +253,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); } @@ -268,6 +285,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, @@ -377,10 +395,15 @@ impl Dispatcher { Message::Viewport(message) => { self.message_handlers.viewport_message_handler.process_message(message, &mut queue, ()); } - Message::NoOp => {} + // Messages + Message::Wake => { + queue.add(PortfolioMessage::PollNodeGraphEvaluation); + 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 @@ -411,10 +434,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/lib.rs b/editor/src/lib.rs index 56e89e7f4e7..1a1cb35c321 100644 --- a/editor/src/lib.rs +++ b/editor/src/lib.rs @@ -11,10 +11,12 @@ mod macros; extern crate log; pub mod application; +pub mod builder; 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/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/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index 2ea0dd74c5e..876755dcbea 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; @@ -246,6 +246,9 @@ pub enum FrontendMessage { UpdateDocumentArtwork { svg: String, }, + UpdateRenderStatus { + status: RenderStatus, + }, UpdateImageData { image_data: Vec, }, @@ -291,8 +294,8 @@ pub enum FrontendMessage { UpdateNodeGraphNodes { nodes: Vec, }, - UpdateNodeGraphErrorDiagnostic { - error: Option, + UpdateNodeGraphErrorDiagnostics { + errors: Vec, }, UpdateVisibleNodes { nodes: 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/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 10731e65efb..cc766c2aa99 100644 --- a/editor/src/messages/future/future_message_handler.rs +++ b/editor/src/messages/future/future_message_handler.rs @@ -6,19 +6,16 @@ use dyn_any::WasmNotSend; use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender, unbounded}; use crate::messages::prelude::*; +use crate::task::Spawner; #[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 { - 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)] @@ -62,52 +59,24 @@ 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, + spawner: Spawner, 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(spawner: Spawner, wake: Wake) -> Self { let (results_sender, results_receiver) = unbounded(); Self { - spawner: default_spawner(), + spawner, 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() { - out.push_back(message); - } - } -} - -impl Default for FutureMessageHandler { - fn default() -> Self { - Self::with_wake(noop_wake()) - } } impl std::fmt::Debug for FutureMessageHandler { @@ -117,87 +86,25 @@ 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 } => { - #[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.spawner.spawn(Box::pin(async move { + let message = future.await; + let _ = results.unbounded_send(message); + wake(); + })); } - 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); + } } } } 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..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, MessageSpawner, Wake}; +pub use future_message_handler::{FutureMessageHandler, MessageFuture, Wake}; 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 145b75d0731..be58b44e63f 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()) @@ -49,7 +53,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 3765b0ed8f8..eecf69e0b7c 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 2d0a5d6cf57..ec5767eeb94 100644 --- a/editor/src/messages/input_mapper/utility_types/macros.rs +++ b/editor/src/messages/input_mapper/utility_types/macros.rs @@ -163,8 +163,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 8dcf77369fe..6ffda83d1d5 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::*; @@ -18,12 +18,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); @@ -113,19 +107,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 14a5526a941..b8f6399f352 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::consts::DOUBLE_TAP_MILLISECONDS; use crate::messages::input_mapper::utility_types::keyboard::{Key, KeyStates, ModifierKeys}; use crate::messages::input_mapper::utility_types::misc::FrameTimeInfo; @@ -8,6 +8,7 @@ use std::time::Duration; #[derive(ExtractField)] pub struct InputPreprocessorMessageContext<'a> { + pub environment: Environment, pub viewport: &'a ViewportMessageHandler, } @@ -35,12 +36,12 @@ enum DoubleTapState { #[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.double_tap_state = DoubleTapState::Idle; - 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; @@ -57,7 +58,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 { @@ -80,7 +81,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)); @@ -101,7 +102,7 @@ impl<'a> MessageHandler { self.double_tap_state = DoubleTapState::Idle; - 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; @@ -109,7 +110,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; @@ -121,7 +122,7 @@ impl<'a> MessageHandler { self.double_tap_state = DoubleTapState::Idle; - 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; @@ -129,7 +130,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; @@ -143,7 +144,7 @@ impl<'a> MessageHandler { self.double_tap_state = DoubleTapState::Idle; - 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; @@ -199,7 +200,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 @@ -208,14 +209,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), }; @@ -238,6 +239,7 @@ impl InputPreprocessorMessageHandler { #[cfg(test)] mod test { use super::DoubleTapState; + use crate::application::DEFAULT_ENVIRONMENT; use crate::consts::DOUBLE_TAP_MILLISECONDS; use crate::messages::input_mapper::utility_types::keyboard::{Key, ModifierKeys}; use crate::messages::input_mapper::utility_types::pointer::{EditorPointerState, MouseKeys}; @@ -257,6 +259,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); @@ -276,6 +279,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); @@ -295,6 +299,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); @@ -316,6 +321,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); @@ -336,6 +342,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); @@ -381,6 +388,7 @@ mod test { message, responses, InputPreprocessorMessageContext { + environment: DEFAULT_ENVIRONMENT, viewport: &ViewportMessageHandler::default(), }, ); diff --git a/editor/src/messages/layout/layout_message_handler.rs b/editor/src/messages/layout/layout_message_handler.rs index afd26eda4b8..0b5c530dcb6 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::math_expression::{ MathExpressionTokens, math_expression_completions, math_expression_error, math_expression_error_ranges, math_expression_number, math_expression_tokens, @@ -12,6 +13,7 @@ use std::collections::HashMap; #[derive(ExtractField)] pub struct LayoutMessageContext<'a> { + pub environment: Environment, pub action_input_mapping: &'a dyn Fn(&MessageDiscriminant) -> Option, } @@ -23,6 +25,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 { @@ -36,7 +39,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 @@ -49,11 +52,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) { @@ -575,6 +578,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(); @@ -605,12 +609,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 549cb4628dc..0dc7a14c070 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::frontend::IconName; use crate::messages::input_mapper::utility_types::keyboard::KeysGroup; use crate::messages::portfolio::document::node_graph::utility_types::FrontendGraphDataType; @@ -996,7 +996,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 @@ -1033,7 +1033,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 @@ -1041,23 +1041,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); } } } @@ -1065,29 +1065,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::NodeSection(NodeSection { 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/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/document_diff.rs b/editor/src/messages/portfolio/document/document_diff.rs index a128a975d3a..3f5f2938b4c 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/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index f99f053197d..ba8505e16e6 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::{ANGLE_STEP_MAX, ANGLE_STEP_MIN, GroupFolderType 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, }; @@ -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, @@ -246,6 +247,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, @@ -263,6 +265,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, @@ -4336,6 +4339,7 @@ fn deserialize_collapsed_layers<'de, D: serde::Deserializer<'de>>(deserializer: mod document_message_handler_tests { use super::*; use crate::consts::{BIG_NUDGE_AMOUNT, NUDGE_AMOUNT}; + use crate::messages::frontend::utility_types::RenderStatus; use crate::messages::portfolio::document::utility_types::misc::GridType; use crate::test_utils::test_prelude::*; @@ -4576,8 +4580,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/document/navigation/navigation_message_handler.rs b/editor/src/messages/portfolio/document/navigation/navigation_message_handler.rs index 09db062282b..e03e9745d35 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, @@ -39,6 +40,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, @@ -189,7 +191,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/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 0a6c87069ce..b3d7021c6f8 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 @@ -42,7 +42,6 @@ pub(super) fn post_process_nodes(custom: Vec) -> HashMap fields, description, inspector, - context_features, output_fields, .. } = metadata; @@ -90,7 +89,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/node_graph/node_graph_message.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs index 60d26bfb0ff..a3be59a0dae 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; @@ -305,6 +305,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 8a0db8caecd..c4228ccc686 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 @@ -8,7 +8,7 @@ use crate::messages::portfolio::document::graph_operation::utility_types::Modify use crate::messages::portfolio::document::node_graph::document_node_definitions::{ DefinitionIdentifier, NodeInspectorContext, 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::{ @@ -118,6 +118,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. @@ -1972,6 +1973,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 }); @@ -1986,8 +1990,8 @@ impl<'a> MessageHandler> for NodeG self.frontend_nodes = nodes.iter().map(|node| node.id).collect(); responses.add(FrontendMessage::UpdateNodeGraphNodes { nodes }); - 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); @@ -2452,6 +2456,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 errors = self.node_graph_errors(network_interface, breadcrumb_network_path); + responses.add(FrontendMessage::UpdateNodeGraphErrorDiagnostics { errors }); + responses.add(NodeGraphMessage::UpdateVisibleNodes); + } + } + } NodeGraphMessage::UpdateActionButtons => { if selection_network_path == breadcrumb_network_path { self.update_graph_bar_left(network_interface, breadcrumb_network_path, responses); @@ -3191,28 +3205,47 @@ impl NodeGraphMessageHandler { Some(subgraph_names) } - fn node_graph_error(&self, network_interface: &mut NodeNetworkInterface, breadcrumb_network_path: &[NodeId]) -> Option { - let graph_error = network_interface + 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_errors: Vec<_> = 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() + .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 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 - 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( @@ -3411,6 +3444,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/node_inspector.rs b/editor/src/messages/portfolio/document/node_graph/node_inspector.rs index 09fe85c5535..bca938a1496 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_inspector.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_inspector.rs @@ -448,7 +448,6 @@ pub(crate) fn inspector_row_from_type(node_id: NodeId, index: usize, ty: &Type, }, Type::Generic(_) => vec![TextLabel::new("Generic Type (Not Supported)").widget_instance()].into(), Type::Fn(_, out) => return inspector_row_from_type(node_id, index, out, options, context), - Type::Future(out) => return inspector_row_from_type(node_id, index, out, options, context), }; extra_widgets.push(widgets); @@ -3867,8 +3866,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/editor/src/messages/portfolio/document/node_graph/utility_types.rs b/editor/src/messages/portfolio/document/node_graph/utility_types.rs index 549df5797cd..33453dd639f 100644 --- a/editor/src/messages/portfolio/document/node_graph/utility_types.rs +++ b/editor/src/messages/portfolio/document/node_graph/utility_types.rs @@ -193,6 +193,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/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/document/utility_types/network_interface.rs b/editor/src/messages/portfolio/document/utility_types/network_interface.rs index a5ce50be696..462b1295a31 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface.rs @@ -53,7 +53,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::registry::MULTI_OUTPUT_NODES; use graphene_std::transform::Footprint; 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 49e48770f53..9f517e885f6 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 1f9b9867158..0debcf67d48 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 @@ -211,17 +211,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 905d1accd55..395e76b9803 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 @@ -515,14 +515,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 f75929b6760..b5ad0a23f3d 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 ea551eb9324..3d572f5f8e1 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 Inspector widgets for each input. Kept the same length as `inputs`. @@ -60,7 +58,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(), @@ -117,7 +114,7 @@ impl NodeTemplate { implementation, visible, skip_deduplication, - context_features, + context_dependencies: _, original_location: _, } = document_node; let DocumentNodePersistentMetadata { @@ -163,7 +160,6 @@ impl NodeTemplate { implementation, visible, skip_deduplication, - context_features, display_name, input_metadata, output_names, @@ -182,7 +178,6 @@ impl NodeTemplate { implementation, visible, skip_deduplication, - context_features, display_name, mut input_metadata, output_names, @@ -203,7 +198,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 668dc412e78..08536927852 100644 --- a/editor/src/messages/portfolio/document_migration.rs +++ b/editor/src/messages/portfolio/document_migration.rs @@ -3214,15 +3214,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/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 5ae23bea9d9..00c50133d4d 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; @@ -34,6 +35,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, @@ -45,7 +47,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, @@ -69,6 +71,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, @@ -88,6 +91,7 @@ impl MessageHandler> for Portfolio { let document_inputs = DocumentMessageContext { document_id, + environment, ipp, fonts: &self.fonts, executor: &mut self.executor, @@ -143,9 +147,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); @@ -162,16 +164,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. @@ -185,6 +188,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, @@ -994,6 +998,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); @@ -1050,12 +1060,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)] @@ -1224,30 +1247,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 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 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 - } - fn document_details(&self, document_id: DocumentId) -> Option { if let Some(document) = self.documents.get(&document_id) { Some(DocumentInfo { diff --git a/editor/src/messages/prelude.rs b/editor/src/messages/prelude.rs index a4f555dc20e..766da5b616b 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, 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/messages/tool/utility_types.rs b/editor/src/messages/tool/utility_types.rs index a23a0610f35..811cdc11554 100644 --- a/editor/src/messages/tool/utility_types.rs +++ b/editor/src/messages/tool/utility_types.rs @@ -556,7 +556,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)]); @@ -598,10 +598,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, @@ -621,7 +621,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(), @@ -635,7 +635,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, @@ -671,7 +671,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(), @@ -681,7 +681,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, @@ -709,7 +709,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 } } diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index ecb9ba72d6d..71b37dad40f 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::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::*; use glam::{DAffine2, DVec2, UVec2}; @@ -6,7 +7,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; @@ -19,14 +20,12 @@ 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::*; -#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)] pub struct ExecutionRequest { execution_id: u64, render_config: RenderConfig, @@ -34,13 +33,24 @@ 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 { + Pending, + 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, @@ -51,14 +61,15 @@ pub enum NodeGraphUpdate { ExecutionResponse(Box), CompilationResponse(CompilationResponse), EyedropperPreview(Raster), - NodeGraphUpdateMessage(NodeGraphUpdateMessage), } -#[derive(Debug, Default)] +#[derive(Debug)] pub struct NodeGraphExecutor { - runtime_io: NodeRuntimeIO, + sender: Sender, + 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, @@ -100,23 +111,23 @@ 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(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(), - 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(), + pending: None, }; - (node_runtime, node_executor) + (node_executor, node_runtime) } /// Allows the test runner to see if any more executions have been queued. @@ -130,13 +141,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"); } @@ -151,7 +162,7 @@ impl NodeGraphExecutor { let resources = document.resources.registry.clone(); - self.runtime_io + self.sender .send(GraphRuntimeRequest::GraphUpdate(GraphUpdate { network, resources, @@ -172,7 +183,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())?; } @@ -338,7 +349,7 @@ impl NodeGraphExecutor { export_config.size = resolution; // Execute the node graph - self.runtime_io + self.sender .send(GraphRuntimeRequest::GraphUpdate(GraphUpdate { network, resources, @@ -359,14 +370,14 @@ 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) { 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) => { @@ -386,42 +397,77 @@ 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. // 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); + // 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() + .map(|(error, node_path)| NodeEvaluationError { + node_path: node_path.to_vec(), + message: error.to_string(), + }) + .collect(), + ExecutionResult::Pending | ExecutionResult::Value(_) | ExecutionResult::Panic(_) => Vec::new(), + }; + responses.add(NodeGraphMessage::UpdateEvaluationErrors { errors }); + } + let node_graph_output = match result { + 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)); + 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 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; } }; 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 { - // 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, execution_context.document_to_viewport, responses)?; + 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, execution_context.document_to_viewport, responses), + (None, _) => {} } responses.add(DeferMessage::TriggerGraphRun { execution_id, @@ -462,7 +508,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, }; @@ -476,11 +523,8 @@ impl NodeGraphExecutor { let (data, width, height) = raster.to_flat_u8(); responses.add(EyedropperToolMessage::PreviewImage { data, width, height }); } - NodeGraphUpdate::NodeGraphUpdateMessage(_) => {} } } - - Ok(()) } // TODO: Eventually remove this document upgrade code @@ -548,7 +592,7 @@ impl NodeGraphExecutor { let resources = document.resources.registry.clone(); if self - .runtime_io + .sender .send(GraphRuntimeRequest::GraphUpdate(GraphUpdate { network, resources, @@ -667,9 +711,11 @@ impl NodeGraphExecutor { self.node_graph_hash = 0; } - fn process_node_graph_output(&mut self, node_graph_output: TaggedValue, document_to_viewport: DAffine2, responses: &mut VecDeque) -> Result<(), String> { + fn process_node_graph_output(&mut self, node_graph_output: TaggedValue, document_to_viewport: DAffine2, 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 { @@ -697,7 +743,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 { @@ -730,10 +780,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, @@ -773,15 +823,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/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index 26f1307866f..b9884efc915 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::{Spawn, Spawner, Task}; use glam::{DAffine2, DVec2, UVec2}; use graph_craft::application_io::resource::ResourceRegistry; use graph_craft::application_io::{PlatformApplicationIo, PlatformEditorApi}; @@ -8,23 +9,24 @@ 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; -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; use graphene_std::renderer::{Render, RenderParams, RenderSvgSegmentList, SvgRender, SvgSegment}; +use graphene_std::runtime::AsyncRuntime; use graphene_std::transform::RenderQuality; use graphene_std::vector::style::RenderMode; use graphene_std::vector::{Vector, VectorGeometry, paths_from_list_row_attributes}; -use graphene_std::{Artboard, Context, Graphic}; -use interpreted_executor::dynamic_executor::{DynamicExecutor, IntrospectError, ResolvedDocumentNodeTypesDelta}; +use graphene_std::{Artboard, Context, Graphic, NodeError, Poll}; +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::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`]`. @@ -36,10 +38,15 @@ pub struct NodeRuntime { #[cfg(not(test))] executor: DynamicExecutor, receiver: Receiver, - sender: InternalNodeGraphUpdateSender, + sender: Sender, + editor_wake: Wake, + dirty: Arc, + pending: Option, + pending_export: Option, editor_preferences: EditorPreferences, old_graph: Option, update_thumbnails: bool, + async_runtime: Option, editor_api: Arc, resources: ResourceRegistry, @@ -93,46 +100,24 @@ 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)); - 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: InternalNodeGraphUpdateSender(sender.clone()), + sender, + editor_wake, + dirty: Default::default(), + pending: None, + pending_export: None, editor_preferences: EditorPreferences::default(), old_graph: None, resources: ResourceRegistry::default(), update_thumbnails: true, + async_runtime: None, editor_api: PlatformEditorApi { editor_preferences: Box::new(EditorPreferences::default()), - node_graph_message_sender: Box::new(InternalNodeGraphUpdateSender(sender)), #[cfg(not(test))] application_io: None, @@ -158,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; } @@ -185,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 @@ -201,13 +196,12 @@ 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(); 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 { @@ -222,14 +216,23 @@ 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; - 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, .. }) => { + 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()) @@ -237,7 +240,38 @@ impl NodeRuntime { render_config.export_format = ExportFormat::Svg; } - let result = self.execute_network(render_config).await; + 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; + 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; + } + }; + + 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 { @@ -248,35 +282,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).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::Pending | 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 @@ -285,59 +299,54 @@ 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; - - self.sender.send_eyedropper_preview(raster_cpu); - continue; + let raster_cpu = Raster::new_gpu(texture).convert(Footprint::BOUNDLESS, executor.clone()).await; + + 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; + } - 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; } } @@ -345,8 +354,8 @@ impl NodeRuntime { None } - async fn update_network(&mut self, graph: NodeNetwork) -> Result { - let mut scoped_network = wrap_network_in_scope(graph, self.editor_api.clone()); + fn update_network(&mut self, graph: NodeNetwork) -> Result { + 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())); @@ -368,25 +377,48 @@ impl NodeRuntime { .collect::>(); assert_ne!(proto_network.nodes.len(), 0, "No proto nodes exist?"); - self.executor.update(proto_network).await.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 { + fn execute_network(&mut self, render_config: RenderConfig) -> Result, String> { 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!(()) => (&self.executor).execute(()).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())) + .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())); + } + pending.extend(error.upstream().iter().rev().map(|upstream| (origin, upstream))); + } + errors.into_boxed_slice() + } + /// 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))); @@ -570,42 +602,45 @@ 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); - } -} - 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(); } + + pub(crate) fn replace_task_executor(&mut self, spawner: Spawner, wake: Wake) { + let spawn = NodeTaskExecutor { + spawner, + dirty: self.dirty.clone(), + wake, + }; + self.async_runtime = Some(AsyncRuntime::new(Arc::new(spawn))); + } + + fn async_runtime(&self) -> &AsyncRuntime { + self.async_runtime.as_ref().expect("the task executor is installed before the first graph update") + } +} + +struct NodeTaskExecutor { + spawner: Spawner, + dirty: Arc, + wake: Wake, +} + +impl Spawn for NodeTaskExecutor { + fn spawn(&self, task: Task) { + let dirty = self.dirty.clone(); + let wake = self.wake.clone(); + self.spawner.spawn(Box::pin(async move { + task.await; + dirty.store(true, Ordering::Release); + wake(); + })); + } } /// Which node is inspected and which monitor node is used (if any) for the current execution 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 new file mode 100644 index 00000000000..f571fb1c94d --- /dev/null +++ b/editor/src/task.rs @@ -0,0 +1,4 @@ +pub use graphene_std::runtime::{Spawn, Task}; +use std::sync::Arc; + +pub type Spawner = Arc; diff --git a/editor/src/test_utils.rs b/editor/src/test_utils.rs index add951a6a89..8c9cdb5adc3 100644 --- a/editor/src/test_utils.rs +++ b/editor/src/test_utils.rs @@ -1,4 +1,6 @@ -use crate::application::Editor; +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; @@ -8,30 +10,59 @@ 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; +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 { init_test_logger(); - let (mut editor, runtime) = Editor::new_local_executor(); - - editor.handle_message(PortfolioMessage::Init); + let woken = Arc::new(AtomicBool::new(false)); + let wake = woken.clone(); + let (editor, runtime) = Builder::new() + .with_editor( + EditorBuilder::new() + .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())), + ) + .with_runtime(RuntimeBuilder::new().with_wake(|| {}).with_application_io(PlatformApplicationIo::default())) + .with_spawner(BlockingSpawner) + .with_random_seed(0) + .build(); + let mut editor = Self { editor, runtime, woken }; + + 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 }); + + editor + } - Self { editor, runtime } + 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 } /// Submit an evaluation to the executor, run the runtime, and process the results. @@ -53,11 +84,10 @@ impl EditorTestUtils { } self.runtime.run().await; - let mut messages = VecDeque::new(); - if let Err(e) = self.editor.poll_node_graph_evaluation(&mut messages) { - return Err(format!("Graph should render\n\n{e}")); + let frontend_messages = self.dispatch(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| self.editor.handle_message(message)); for message in frontend_messages { message.check_node_graph_error(); @@ -81,7 +111,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_until_finished().await { @@ -379,9 +409,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:?}"); } } } @@ -440,6 +470,13 @@ pub fn capture_logged_errors(operation: impl FnOnce() -> T) -> (T, Vec {/if} - {#if isWeb} + {#if isWeb && $document.renderStatus === "Rendering"} 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}
@@ -961,6 +982,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/components/views/Graph.svelte b/frontend/src/components/views/Graph.svelte index 35eeb69735b..2d276ee7455 100644 --- a/frontend/src/components/views/Graph.svelte +++ b/frontend/src/components/views/Graph.svelte @@ -420,14 +420,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/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"); diff --git a/frontend/src/stores/node-graph.ts b/frontend/src/stores/node-graph.ts index 79fd0e191b7..be47cfe39ae 100644 --- a/frontend/src/stores/node-graph.ts +++ b/frontend/src/stores/node-graph.ts @@ -13,7 +13,7 @@ type NodeGraphStoreState = { box: BoxSelection | undefined; clickTargets: FrontendClickTargets | undefined; contextMenuInformation: ContextMenuInformation | undefined; - error: NodeGraphErrorDiagnostic | undefined; + errors: NodeGraphErrorDiagnostic[]; layerWidths: Map; chainWidths: Map; hasLeftInputWire: Map; @@ -32,7 +32,7 @@ const initialState: NodeGraphStoreState = { box: undefined, clickTargets: undefined, contextMenuInformation: undefined, - error: undefined, + errors: [], layerWidths: new Map(), chainWidths: new Map(), hasLeftInputWire: new Map(), @@ -150,9 +150,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; }); }); @@ -244,7 +244,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"); 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..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")] @@ -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) } @@ -177,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 d8f5adf86fb..e39a0400810 100644 --- a/frontend/wrapper/src/helpers.rs +++ b/frontend/wrapper/src/helpers.rs @@ -1,16 +1,14 @@ #[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}; use js_sys::{Object, Reflect}; use std::cell::RefCell; use std::collections::HashSet; @@ -38,30 +36,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,52 +51,38 @@ 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; } - if !editor::node_graph_executor::run_node_graph().await.0 { - return; - } - - 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}"); - } - } - - // Clear the error display if there are no more errors - if !messages.is_empty() { - crate::NODE_GRAPH_ERROR_DISPLAYED.store(false, Ordering::SeqCst); - } + let Some(mut runtime) = crate::NODE_RUNTIME.try_lock() else { return }; + let Some(runtime) = runtime.as_mut() else { return }; + runtime.run().await; +} - // 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); - } +#[cfg(all(not(feature = "native"), target_family = "wasm"))] +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); + } +} - // If the editor cannot be borrowed then it has encountered a panic - we should just ignore new dispatches +/// 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(Message::Wake)); }); } -/// Web wake callback: queues a microtask that dispatches [`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 async_wake_callback() -> Wake { - use std::sync::Arc; - Arc::new(|| { - wasm_bindgen_futures::spawn_local(async { - wrapper(|wrapper| wrapper.dispatch(FutureMessage::Wake)); - }); - }) +pub(crate) fn wake_runtime() { + wasm_bindgen_futures::spawn_local(run_node_runtime()); } thread_local! { diff --git a/frontend/wrapper/src/lib.rs b/frontend/wrapper/src/lib.rs index 0785d8efab5..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,8 +27,9 @@ 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); thread_local! { #[cfg(not(feature = "native"))] @@ -60,25 +63,12 @@ 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) { - 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; } diff --git a/node-graph/README.md b/node-graph/README.md index addc4f772c4..11312e94de0 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/graph-craft/src/document.rs b/node-graph/graph-craft/src/document.rs index 6297ceab3e4..c0228aedec8 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, @@ -93,7 +92,7 @@ impl Default for DocumentNode { visible: true, skip_deduplication: Default::default(), original_location: OriginalLocation::default(), - context_features: Default::default(), + context_dependencies: Default::default(), } } } @@ -167,7 +166,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(), } } } @@ -216,12 +215,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), } } } @@ -958,6 +959,11 @@ impl NodeNetwork { self.nodes.insert(id, node); return; }; + 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( @@ -1077,6 +1083,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; @@ -1329,7 +1336,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/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 729dcebbaea..27b6b437e00 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, ContextFeatures, MemoHash, Node, 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_animation::AnimationCurve; @@ -23,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; @@ -116,7 +114,11 @@ 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)] + Runtime(RuntimeHandle), #[serde(skip)] EditorApi(Arc), /// Only used by the `resource` node, should never be serialized @@ -149,7 +151,9 @@ 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::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), @@ -157,9 +161,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 @@ -222,8 +226,10 @@ 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::EditorApi(x) => Box::new(x), + 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)), } } @@ -290,8 +296,10 @@ 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::EditorApi(x) => Arc::new(x), + 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)), } } @@ -321,8 +329,10 @@ macro_rules! tagged_value { Self::RenderOutput(_) => item!(RenderOutput), Self::NodeIdPath(_) => item!(NodeIdPath), Self::DocumentNode(_) => concrete!(DocumentNode), - Self::ContextFeatures(_) => item!(ContextFeatures), - Self::EditorApi(_) => item!(&PlatformEditorApi), + Self::ContextModification(_) => item!(ContextModification), + Self::SourceId(_) => item!(SourceId), + Self::Runtime(_) => item!(RuntimeHandle), + Self::EditorApi(_) => item!(Arc), Self::ResourceHash(_) => item!(ResourceHash), }; @@ -331,39 +341,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())), } } @@ -429,7 +438,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 { @@ -487,7 +495,9 @@ 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::Runtime(_) => "Runtime".to_string(), Self::EditorApi(_) => "PlatformEditorApi".to_string(), Self::ResourceHash(hash) => format!("ResourceHash({hash:?})"), } @@ -718,7 +728,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), } @@ -898,12 +907,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 { @@ -911,23 +919,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, @@ -1002,7 +993,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 702c4f8835a..325b6e2a690 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/graph-craft/src/proto.rs b/node-graph/graph-craft/src/proto.rs index 9dd4e8dbe80..e185573ee18 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(), } } } @@ -179,7 +179,7 @@ impl ProtoNode { ..Default::default() }, skip_deduplication: false, - context_features: Default::default(), + context_dependencies: Default::default(), } } @@ -288,7 +288,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(); @@ -318,7 +318,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 { @@ -345,60 +345,80 @@ 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 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::Nodes(items) => items.clone(), - ConstructionArgs::Inline(_) => return (context_features.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 &= !context_features.inject; - // Add requirements we have - new_deps |= context_features.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); } } 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); + 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) @@ -700,7 +720,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); } @@ -734,8 +754,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. @@ -1045,6 +1063,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/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/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/export.rs b/node-graph/graphene-cli/src/export.rs index 0ffafbea050..60e000b08a8 100644 --- a/node-graph/graphene-cli/src/export.rs +++ b/node-graph/graphene-cli/src/export.rs @@ -1,9 +1,12 @@ +use crate::TokioSpawner; 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 graphene_std::runtime::AsyncRuntime; +use graphene_std::{NodeError, Poll}; use image::ImageFormat; use interpreted_executor::dynamic_executor::DynamicExecutor; use std::error::Error; @@ -73,7 +76,14 @@ pub struct ExportSettings { } /// Renders the document at the given animation time, as SVG text for an SVG export and as pixels for every other file type. -async fn render_frame(executor: &DynamicExecutor, wgpu_executor: &wgpu_executor::WgpuExecutor, settings: ExportSettings, animation_time: Duration) -> Result> { +async fn render_frame( + executor: &DynamicExecutor, + wgpu_executor: &wgpu_executor::WgpuExecutor, + settings: ExportSettings, + animation_time: Duration, + runtime: &AsyncRuntime, + spawner: &TokioSpawner, +) -> Result> { let ExportSettings { file_type, scale, @@ -100,13 +110,13 @@ async fn render_frame(executor: &DynamicExecutor, wgpu_executor: &wgpu_executor: } // Execute the graph and extract its output - match executor.execute(render_config.into_context()).await? { + match execute_until_settled(executor, runtime, spawner, render_config).await? { TaggedValue::RenderOutput(output) => match output.data { RenderOutputType::Svg { svg, .. } => Ok(RenderedFrame::Svg(svg)), 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(); Ok(RenderedFrame::Raster { data, width, height }) } @@ -174,8 +184,15 @@ fn encode_raster(file_type: FileType, data: Vec, width: u32, height: u32, tr Ok(cursor.into_inner()) } -pub async fn export_document(executor: &DynamicExecutor, wgpu_executor: &wgpu_executor::WgpuExecutor, output_path: PathBuf, settings: ExportSettings) -> Result<(), Box> { - let frame = render_frame(executor, wgpu_executor, settings, Duration::ZERO).await?; +pub async fn export_document( + executor: &DynamicExecutor, + wgpu_executor: &wgpu_executor::WgpuExecutor, + output_path: PathBuf, + settings: ExportSettings, + runtime: &AsyncRuntime, + spawner: &TokioSpawner, +) -> Result<(), Box> { + let frame = render_frame(executor, wgpu_executor, settings, Duration::ZERO, runtime, spawner).await?; write_frame(&output_path, settings, frame)?; log::info!("Exported to: {}", output_path.display()); @@ -217,6 +234,8 @@ pub async fn export_gif( output_path: PathBuf, settings: ExportSettings, animation: AnimationParams, + runtime: &AsyncRuntime, + spawner: &TokioSpawner, ) -> Result<(), Box> { use image::codecs::gif::{GifEncoder, Repeat}; use image::{Frame, RgbaImage}; @@ -238,7 +257,7 @@ pub async fn export_gif( log::debug!("Rendering frame {}/{} at time {:?}", frame_idx + 1, animation.frames, animation_time); - let RenderedFrame::Raster { data, width, height } = render_frame(executor, wgpu_executor, settings, animation_time).await? else { + let RenderedFrame::Raster { data, width, height } = render_frame(executor, wgpu_executor, settings, animation_time, runtime, spawner).await? else { return Err("A GIF frame must render as pixels".into()); }; @@ -265,6 +284,8 @@ pub async fn export_image_sequence( output_path: PathBuf, settings: ExportSettings, animation: AnimationParams, + runtime: &AsyncRuntime, + spawner: &TokioSpawner, ) -> Result<(), Box> { log::info!("Exporting image sequence: {} frames at {} fps", animation.frames, animation.fps); @@ -280,7 +301,7 @@ pub async fn export_image_sequence( // Print progress to stderr (overwrites previous line) eprint!("\rRendering frame {}/{}...", frame_idx + 1, animation.frames); - let frame = render_frame(executor, wgpu_executor, settings, animation_time).await?; + let frame = render_frame(executor, wgpu_executor, settings, animation_time, runtime, spawner).await?; let frame_path = directory.join(format!("{stem}_{frame_idx:0digits$}.{extension}")); write_frame(&frame_path, settings, frame)?; } @@ -292,6 +313,32 @@ pub async fn export_image_sequence( 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()?; + } + 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 +} + #[cfg(test)] mod tests { use super::*; diff --git a/node-graph/graphene-cli/src/main.rs b/node-graph/graphene-cli/src/main.rs index 28655426bdb..c42b596bc2d 100644 --- a/node-graph/graphene-cli/src/main.rs +++ b/node-graph/graphene-cli/src/main.rs @@ -5,27 +5,19 @@ 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::*; 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 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; use std::path::PathBuf; -use std::sync::Arc; - -struct UpdateLogger {} - -impl NodeGraphUpdateSender for UpdateLogger { - fn send(&self, message: NodeGraphUpdateMessage) { - println!("{message:?}"); - } -} +use std::sync::{Arc, Mutex}; #[derive(Debug, Parser)] #[clap(name = "graphene-cli", version)] @@ -183,10 +175,11 @@ 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())?; + 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 { Command::Compile { print_proto, .. } => { @@ -218,6 +211,7 @@ async fn main() -> Result<(), Box> { // Create executor let executor = create_executor(proto_graph)?; + runtime.retain_sources(executor.sources()); if fps <= 0. { return Err("Fps number must be positive".into()); @@ -232,14 +226,14 @@ async fn main() -> Result<(), Box> { }; let animation = export::AnimationParams::new(fps, frames, duration); if file_type == export::FileType::Gif { - export::export_gif(&executor, wgpu_executor_ref, output, settings, animation).await?; + export::export_gif(&executor, wgpu_executor_ref, output, settings, animation, &runtime, &spawner).await?; } else if animation.frames > 1 { if file_type == export::FileType::Ico { return Err("ICO can't be exported as an image sequence".into()); } - export::export_image_sequence(&executor, wgpu_executor_ref, output, settings, animation).await?; + export::export_image_sequence(&executor, wgpu_executor_ref, output, settings, animation, &runtime, &spawner).await?; } else { - export::export_document(&executor, wgpu_executor_ref, output, settings).await?; + export::export_document(&executor, wgpu_executor_ref, output, settings, &runtime, &spawner).await?; } } _ => unreachable!("All other commands should be handled before this match statement is run"), @@ -277,10 +271,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). @@ -296,6 +290,26 @@ 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) } + +#[derive(Default)] +pub(crate) struct TokioSpawner(Mutex>>); + +impl Spawn for TokioSpawner { + fn spawn(&self, task: 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 + } +} diff --git a/node-graph/interpreted-executor/benches/benchmark_util.rs b/node-graph/interpreted-executor/benches/benchmark_util.rs index 25c62f55f84..cad6af3acc0 100644 --- a/node-graph/interpreted-executor/benches/benchmark_util.rs +++ b/node-graph/interpreted-executor/benches/benchmark_util.rs @@ -1,20 +1,20 @@ 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; +use graphene_std::runtime::{AsyncRuntime, Spawn, Task}; use interpreted_executor::dynamic_executor::DynamicExecutor; use interpreted_executor::util::wrap_network_in_scope; pub fn setup_network(name: &str) -> (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); - let executor = block_on(DynamicExecutor::new(proto_network.clone())).unwrap(); + let executor = DynamicExecutor::new(proto_network.clone()).unwrap(); (executor, proto_network) } @@ -27,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) {} +} 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 7f585656653..048abcbda32 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,10 +33,10 @@ 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)))); + 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/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.rs b/node-graph/interpreted-executor/src/dynamic_executor.rs index ffa2c7ddb50..aa79de34921 100644 --- a/node-graph/interpreted-executor/src/dynamic_executor.rs +++ b/node-graph/interpreted-executor/src/dynamic_executor.rs @@ -1,10 +1,10 @@ use crate::node_registry; -use dyn_any::StaticType; +use core_types::{NodeError, Poll, SourceId, WasmNotSend}; 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::{ConstructionArgs, GraphError, NodeContainer, ProtoNetwork, ProtoNode, SharedNodeContainer, TypeErasedBox, TypingContext}; use graph_craft::proto::{GraphErrorType, GraphErrors}; use std::collections::{HashMap, HashSet}; use std::error::Error; @@ -20,6 +20,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 +30,7 @@ impl Default for DynamicExecutor { tree: Default::default(), typing_context: TypingContext::new(&node_registry::NODE_REGISTRY), orphaned_nodes: HashSet::new(), + sources: Vec::new(), } } } @@ -48,23 +50,29 @@ 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 sources = proto_network.source_ids().copied().collect(); + let tree = BorrowTree::new(proto_network, &typing_context)?; Ok(Self { tree, 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 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 +95,8 @@ 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))?; + 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())); for node_id in old_to_remove { @@ -133,29 +138,34 @@ 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 +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> { + self.tree.eval_tagged_value(self.output, &input).map_err(|error| error.into()) } } + pub struct InputMapping {} #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -188,8 +198,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. @@ -197,30 +207,30 @@ 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, } 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()); } @@ -233,40 +243,48 @@ 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`]. - 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. @@ -293,10 +311,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"); @@ -325,7 +343,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); @@ -393,22 +411,15 @@ 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(); 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) - }; - self.store_node(node, id, path.into()); + let upcasted = UpcastNode::new(value.to_owned()); + let node = NodeContainer::new(Box::new(upcasted) as TypeErasedBox<'_>); + self.store_node(node, id, path.into(), Box::default()); } ConstructionArgs::Inline(_) => unimplemented!("Inline nodes are not supported yet"), ConstructionArgs::Nodes(ids) => { @@ -421,15 +432,15 @@ 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()); + 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 1b90375d533..adc40f81d60 100644 --- a/node-graph/interpreted-executor/src/dynamic_executor/test.rs +++ b/node-graph/interpreted-executor/src/dynamic_executor/test.rs @@ -11,10 +11,9 @@ 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), ())); + let result: Option> = 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] @@ -44,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"); } @@ -67,10 +66,10 @@ 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)); + let result: Option> = tree.eval(NodeId(1), &context); assert!(result.is_some(), "The promoted wire should execute end-to-end"); } @@ -97,10 +96,10 @@ 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)); + 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"); } @@ -131,10 +130,10 @@ 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)); + let result: Option> = tree.eval(NodeId(3), &context); assert!(result.is_some(), "The zipped mapped variant should execute end-to-end"); } @@ -144,10 +143,10 @@ 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)); + let result: Option> = tree.eval(NodeId(3), &context); assert!(result.is_some(), "The rank-0 variant should execute and stay rank 0"); } @@ -192,10 +191,10 @@ 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)); + 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"); @@ -212,10 +211,10 @@ 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)); + 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"); @@ -245,10 +244,10 @@ 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)); + 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"); } @@ -260,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; @@ -315,10 +314,10 @@ 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)); + 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"); @@ -339,10 +338,10 @@ 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)); + let result: Option> = tree.eval(NodeId(1), &context); assert!(result.is_some(), "The position should arrive as an Item single-anchor path"); } @@ -364,10 +363,10 @@ 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)); + 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 { @@ -398,10 +397,10 @@ 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)); + 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"); } @@ -443,10 +442,10 @@ 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)); + 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"); @@ -479,10 +478,10 @@ 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)); + 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"); @@ -509,10 +508,10 @@ 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)); + 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"); } @@ -531,10 +530,10 @@ 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)); + 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"); } @@ -554,10 +553,10 @@ 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)); + let result: Option> = tree.eval(NodeId(1), &context); assert_eq!(result.map(|item| *item.element()), Some(expected), "{value} should embed as exactly {expected}"); } } @@ -578,10 +577,10 @@ 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)); + 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}`"); } } @@ -603,10 +602,10 @@ 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)); + 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"); } @@ -625,10 +624,10 @@ 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)); + let result: Option> = tree.eval(NodeId(1), &context); assert_eq!(result.map(|item| *item.element()), Some(3.), "The value should arrive as an Item"); } @@ -651,10 +650,10 @@ 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)); + let result: Option>> = tree.eval(NodeId(1), &context); assert!(result.is_some(), "The modification should arrive as an Item"); } @@ -675,10 +674,10 @@ 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)); + 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::(), @@ -692,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"); } @@ -733,10 +732,10 @@ 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)); + 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(); @@ -779,9 +778,9 @@ 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 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"); @@ -814,9 +813,9 @@ 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)); + let result: Option> = tree.eval(NodeId(3), &context); assert!(result.is_some(), "The unbundled stack should flow into Extend as a List"); } @@ -856,9 +855,152 @@ 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 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"); } + +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))); +} + +#[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); +} + +#[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))); +} + +#[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))); +} + +#[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)]]); +} diff --git a/node-graph/interpreted-executor/src/lib.rs b/node-graph/interpreted-executor/src/lib.rs index ddf05222e52..f0642d2f44f 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; #[test] fn double_number() { @@ -46,6 +45,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 7a23e20dd74..aff56cb4f3d 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -27,9 +27,10 @@ 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; // TODO: turn into hashmap fn node_registry() -> HashMap> { @@ -80,17 +81,19 @@ 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 => 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<&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]), + 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 +138,15 @@ 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>]), + 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]), 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. @@ -158,16 +163,14 @@ 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()); - let params = vec![fn_type_fut!(Context, $ty)]; - let node_io = NodeIO::<'_, Context>::to_async_node_io(&node, params); + let node = graphene_core::ops::PassthroughNode::new(graphene_std::any::PanicNode::::new()); + let params = vec![fn_type!(Context, $ty)]; + let node_io = NodeIO::::to_node_io(&node, params); node_io }, ) @@ -176,22 +179,20 @@ 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::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 params = vec![fn_type!(Context, $from)]; + let node_io = NodeIO::::to_node_io(&node, params); node_io }, ) @@ -211,22 +212,20 @@ 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::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 params = vec![fn_type!(Context, $from)]; + let node_io = NodeIO::::to_node_io(&node, params); node_io }, ) @@ -238,19 +237,14 @@ 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::< - Context, - core::pin::Pin> + Send>>, - >::new()); - let params = vec![fn_type_fut!(Context, Item<$element>)]; - let node_io = NodeIO::<'_, Context>::to_async_node_io(&node, params); + let node = graphene_core::ops::ItemToListNode::new(graphene_std::any::PanicNode::>::new()); + let params = vec![fn_type!(Context, Item<$element>)]; + let node_io = NodeIO::::to_node_io(&node, params); node_io }, ) @@ -262,19 +256,14 @@ 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::< - Context, - core::pin::Pin> + Send>>, - >::new()); - let params = vec![fn_type_fut!(Context, List<$element>)]; - let node_io = NodeIO::<'_, Context>::to_async_node_io(&node, params); + let node = graphene_core::ops::BundleNode::new(graphene_std::any::PanicNode::>::new()); + let params = vec![fn_type!(Context, List<$element>)]; + let node_io = NodeIO::::to_node_io(&node, params); node_io }, ) @@ -286,19 +275,14 @@ 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::< - Context, - core::pin::Pin>> + Send>>, - >::new()); - let params = vec![fn_type_fut!(Context, Item>)]; - let node_io = NodeIO::<'_, Context>::to_async_node_io(&node, params); + let node = graphene_core::ops::UnbundleNode::new(graphene_std::any::PanicNode::>>::new()); + let params = vec![fn_type!(Context, Item>)]; + let node_io = NodeIO::::to_node_io(&node, params); node_io }, ) @@ -465,7 +449,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 }}; @@ -644,19 +628,17 @@ 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($( - 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 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/interpreted-executor/src/util.rs b/node-graph/interpreted-executor/src/util.rs index 1c48c990cf2..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![], @@ -30,20 +31,20 @@ 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!(&PlatformEditorApi)))]; NodeNetwork { exports: vec![NodeInput::node(NodeId(1), 0)], diff --git a/node-graph/libraries/application-io/src/lib.rs b/node-graph/libraries/application-io/src/lib.rs index b3df3314d31..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; @@ -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; } @@ -90,13 +76,9 @@ impl RenderConfig { pub fn into_context(self) -> Context<'static> { OwnedContextImpl::default().with_vararg(Box::new(self)).into_context() } -} - -struct Logger; -impl NodeGraphUpdateSender for Logger { - fn send(&self, message: NodeGraphUpdateMessage) { - log::warn!("dispatching message with fallback node graph update sender {message:?}"); + pub fn into_context_with_generations(self, generations: Generations) -> Context<'static> { + OwnedContextImpl::default().with_vararg(Box::new(self)).with_generations(generations).into_context() } } @@ -111,7 +93,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 +103,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 +111,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 +124,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 _) } } diff --git a/node-graph/libraries/core-types/Cargo.toml b/node-graph/libraries/core-types/Cargo.toml index c1e4daec7b5..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 } @@ -58,5 +59,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/context.rs b/node-graph/libraries/core-types/src/context.rs index ab8b0222393..2a573bdb505 100644 --- a/node-graph/libraries/core-types/src/context.rs +++ b/node-graph/libraries/core-types/src/context.rs @@ -1,8 +1,10 @@ +use crate::runtime::{Generations, SourceId}; use crate::transform::Footprint; 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; @@ -45,6 +47,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 +83,8 @@ pub trait ExtractAll: ExtractPointerPosition + ExtractPosition + ExtractIndex + - ExtractVarArgs {} + ExtractVarArgs + + ExtractGenerations {} impl< T: ?Sized // Extract traits @@ -87,7 +94,8 @@ impl< + ExtractPointerPosition + ExtractPosition + ExtractIndex - + ExtractVarArgs, + + ExtractVarArgs + + ExtractGenerations, > ExtractAll for T { } @@ -125,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 // ================ @@ -292,6 +316,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 +376,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 +461,12 @@ impl ExtractVarArgs for ContextImpl<'_> { } } +impl ExtractGenerations for ContextImpl<'_> { + fn try_generations(&self) -> Option<&Generations> { + None + } +} + // ========================================== // EXTRACT TRAIT IMPLS FOR `OwnedContextImpl` // ========================================== @@ -497,6 +539,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 +570,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 +583,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 +603,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,9 +639,20 @@ impl OwnedContextImpl { position, index, varargs: None, + generations: value.try_generations().cloned(), } } + #[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, @@ -601,6 +663,7 @@ impl OwnedContextImpl { position: None, index: None, varargs: None, + generations: None, } } } @@ -671,6 +734,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/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 5de86b6f89f..60fce461052 100644 --- a/node-graph/libraries/core-types/src/lib.rs +++ b/node-graph/libraries/core-types/src/lib.rs @@ -11,8 +11,10 @@ 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; pub mod transfer_curve; pub mod transform; pub mod uuid; @@ -39,22 +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, PollInput, 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; - /// Resets the node, e.g. the LetNode's cache is set to None. - fn reset(&self) {} + 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::() @@ -69,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 { @@ -93,56 +92,53 @@ where inputs, } } - fn to_async_node_io(&self, inputs: Vec) -> NodeIOTypes - where - ::Output: StaticTypeSized, - Self::Output: Future, - { - NodeIOTypes { - call_argument: concrete!(::Static), - return_value: future!(<::Output as StaticTypeSized>::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 3fb436b015b..f5c06e7febc 100644 --- a/node-graph/libraries/core-types/src/ops.rs +++ b/node-graph/libraries/core-types/src/ops.rs @@ -1,49 +1,51 @@ 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::future::Future; 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) } - fn reset(&self) { - self.0.reset(); - } - fn serialize(&self) -> Option> { 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. pub trait Convert: 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`] +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`]. @@ -52,7 +54,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() } } @@ -64,7 +66,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() } } @@ -81,14 +83,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 } } @@ -108,7 +110,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) } } @@ -145,7 +147,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 } } @@ -158,12 +160,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/core-types/src/poll.rs b/node-graph/libraries/core-types/src/poll.rs new file mode 100644 index 00000000000..6a9889afd07 --- /dev/null +++ b/node-graph/libraries/core-types/src/poll.rs @@ -0,0 +1,429 @@ +#![allow(clippy::inline_always)] + +use crate::Node; +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, + upstream: Vec, +} + +impl NodeError { + pub fn new(error: impl Into>) -> Self { + Self { + error: Arc::from(error.into()), + trace: Vec::new(), + upstream: Vec::new(), + } + } + + #[must_use] + pub fn with_upstream(mut self, upstream: Vec) -> Self { + self.upstream = upstream; + self + } + + pub fn track(&mut self, input: usize) { + 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 + } +} + +impl PartialEq for NodeError { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.error, &other.error) && self.trace == other.trace && self.upstream == other.upstream + } +} + +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>, + raised: Cell>, + interrupt: Cell>, + no_partial: bool, +} + +impl Status { + #[must_use] + pub fn new() -> Self { + Self::default() + } + + #[must_use] + 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.track(index); + self.fail(error); + Ok(value) + } + Poll::Pending => Err(self.interrupted(Interrupt::Pending)), + Poll::Error(mut error) => { + error.track(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))); + } + + 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) { + Ok(value) => self.finish(value), + Err(interrupt) => self.bail(interrupt), + } + } + + #[inline(always)] + pub fn finish(self, value: T) -> Poll { + 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), + } + } + + #[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::Error(mut error)) => { + if error.trace.is_empty() { + error.upstream.extend(self.error.take()); + } + Poll::Error(error) + } + (_, Interrupt::Pending) => Poll::Pending, + } + } + + 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 + } +} + +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)) + } +} + +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.track(self.index), + Poll::Error(error) => error.track(self.index), + Poll::Pending | Poll::Final(_) | Poll::Partial(_) => {} + } + state + } +} + +#[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 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 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| { + 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); + } +} diff --git a/node-graph/libraries/core-types/src/registry.rs b/node-graph/libraries/core-types/src/registry.rs index cf2b57ea6c0..870a5cae0a0 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 @@ -18,6 +17,7 @@ pub struct NodeMetadata { pub context_features: Vec, pub memoize: bool, pub inject_scope: bool, + pub asynchronous: bool, pub output_fields: Option, } @@ -63,6 +63,7 @@ pub enum RegistryValueSource { None, Default(&'static str), Scope(&'static str), + SourceId, } #[derive(Clone, Debug)] @@ -174,28 +175,24 @@ pub static MULTI_OUTPUT_NODES: LazyLock = 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; -pub type NodeConstructor = fn(Vec) -> DynFuture<'static, TypeErasedBox<'static>>; +pub type NodeConstructor = fn(Vec) -> TypeErasedBox<'static>; #[derive(Clone)] pub struct NodeContainer { @@ -255,35 +252,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 reset(&self) { - self.node.reset(); + 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> { @@ -299,36 +288,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 reset(&self) { - self.node.reset(); - } - - #[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, @@ -336,40 +295,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 reset(&self) { - self.node.reset(); + 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, @@ -380,9 +325,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/runtime.rs b/node-graph/libraries/core-types/src/runtime.rs new file mode 100644 index 00000000000..346cbabfdb8 --- /dev/null +++ b/node-graph/libraries/core-types/src/runtime.rs @@ -0,0 +1,362 @@ +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}; +use std::pin::Pin; +use std::sync::{Arc, Mutex, PoisonError}; +use std::task::{Context, Waker}; + +#[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, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, graphene_hash::CacheHash, dyn_any::DynAny)] +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); + Self(hasher.finish()) + } +} + +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) { + state.write_usize(Arc::as_ptr(&self.0) as usize); + } +} + +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() + } + + #[must_use] + 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 Slots(Arc>>>); + +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 Slots { + fn clone(&self) -> Self { + Self(Arc::clone(&self.0)) + } +} + +impl std::fmt::Debug for Slots { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Slots").finish_non_exhaustive() + } +} + +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, + { + 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, Slot::Ready(state.clone())); + return state; + } + + let (handle, registration) = AbortHandle::new_pair(); + let slots = Arc::downgrade(&self.0); + runtime.spawn( + source, + Box::pin(async move { + 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)); + } + }), + ); + 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) + } + } + } +} + +#[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() + } + + #[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 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()); + let runtime = AsyncRuntime::new(spawner.clone()); + runtime.retain_sources(&[SOURCE]); + + 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)]); + + 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 = Slots::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 = Slots::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)]); + } + + #[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/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/libraries/core-types/src/value.rs b/node-graph/libraries/core-types/src/value.rs index 51e9d5490cc..26bdd4e62c6 100644 --- a/node-graph/libraries/core-types/src/value.rs +++ b/node-graph/libraries/core-types/src/value.rs @@ -1,102 +1,15 @@ +use crate::Node; 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)) - } -} +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()) } } @@ -112,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()) } } @@ -131,103 +43,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(()), ()); - } -} diff --git a/node-graph/libraries/graphene-hash/src/lib.rs b/node-graph/libraries/graphene-hash/src/lib.rs index 00be14c1129..0e0acc8876f 100644 --- a/node-graph/libraries/graphene-hash/src/lib.rs +++ b/node-graph/libraries/graphene-hash/src/lib.rs @@ -232,6 +232,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/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/libraries/wgpu-executor/src/texture_conversion.rs b/node-graph/libraries/wgpu-executor/src/texture_conversion.rs index 93fb12da34d..d94372bd51e 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}; @@ -133,21 +133,21 @@ 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> { +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> { - async 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 { - async 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,15 +170,15 @@ 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> { +impl Convert>, WgpuExecutor> for List> { + fn convert(self, _: Footprint, _converter: WgpuExecutor) -> List> { self } } /// 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()]); diff --git a/node-graph/node-macro/src/codegen.rs b/node-graph/node-macro/src/codegen.rs index 2f89c8e8b9e..7f222c4544a 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()); @@ -59,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(); @@ -131,9 +145,10 @@ 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); + 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(); @@ -152,9 +167,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(), }) @@ -200,6 +215,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), @@ -229,7 +245,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))), }, @@ -317,17 +333,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 = &#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)); }, } }); + 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() { @@ -381,8 +400,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. @@ -390,28 +407,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; @@ -420,12 +430,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() @@ -441,13 +446,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 @@ -494,13 +514,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 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)); - 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)]) @@ -522,26 +542,119 @@ 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.raise(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.raise(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.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) + } + + #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 output a framed variant collects into. A map kernel pushes one item per frame slot and a `Destructure` struct appends its // fields to its mapped struct. An expander kernel, returning `List`, has its lists concatenated since nested lists aren't supported. let framed_output = || -> (TokenStream2, TokenStream2, TokenStream2, TokenStream2) { - match (parsed.output_element.as_ref(), peel_list(output_type)) { + match (parsed.output_element.as_ref(), peel_list(&output)) { (Some(element_ty), _) => ( quote!(#core_types::list::List<#element_ty>), quote!(#core_types::list::List::with_capacity(__frame_length)), @@ -549,13 +662,13 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn quote!(#core_types::list::List::new()), ), (None, Some(_)) => ( - quote!(#output_type), + quote!(#output), quote!(#core_types::list::List::new()), quote!(__output.extend(__result);), quote!(#core_types::list::List::new()), ), (None, None) => { - let destructure = quote!(<#output_type as #core_types::registry::Destructure>); + let destructure = quote!(<#output as #core_types::registry::Destructure>); ( quote!(#destructure::Mapped), quote!(#destructure::mapped_with_capacity(__frame_length)), @@ -598,28 +711,31 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let (mapped_output_type, initial_output, collect_result, empty_output) = framed_output(); + 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 #empty_output; + return Ok(#empty_output); } 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 @@ -644,7 +760,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), @@ -661,20 +780,22 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let empty_param_check = (!ranked_names.is_empty()).then(|| { quote! { if [#(#ranked_names.len()),*].into_iter().any(|__length| __length == 0) { - return #empty_output; + return Ok(#empty_output); } } }); + 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 @@ -683,12 +804,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 @@ -706,17 +828,18 @@ 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 inspector = &attributes.inspector_string.as_ref().map(|value| quote!(Some(#value))).unwrap_or(quote!(None)); let icon = &attributes.icon.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; // A node returning a `Destructure` struct records its fields as output connectors let output_fields = match may_return_destructure_struct(parsed) { true => { - let output_type = substitute_lifetimes(output_type.clone(), "static"); + let output_type = substitute_lifetimes(output_payload(output_type), "static"); quote! {{ use gcore::registry::{DestructureOutputFields as _, NoOutputFields as _}; (&gcore::registry::OutputFields::<#output_type>(core::marker::PhantomData)).output_fields() @@ -733,7 +856,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 @@ -746,7 +869,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 @@ -763,7 +886,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 { @@ -783,7 +906,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 { @@ -840,7 +963,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 @@ -864,7 +987,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 @@ -896,10 +1019,10 @@ 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, 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 @@ -912,7 +1035,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 { @@ -939,6 +1062,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, output_fields: #output_fields, fields: vec![ #( @@ -1084,6 +1208,14 @@ 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; + } + + 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; @@ -1126,7 +1258,7 @@ fn is_unit_type(ty: &syn::Type) -> bool { /// Whether the node's return type could be a `Destructure` struct, meaning a concrete type other than `()` or a ranked wrapper. fn may_return_destructure_struct(parsed: &ParsedNodeFn) -> bool { - let output_type = &parsed.output_type; + let output_type = &output_payload(&parsed.output_type); let ranked = peel_item(output_type).is_some() || peel_list(output_type).is_some() || matches!(output_type, Type::Path(path) if path.path.is_ident("ListDyn")); let generic = parsed .fn_generics @@ -1138,11 +1270,14 @@ fn may_return_destructure_struct(parsed: &ParsedNodeFn) -> 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( parsed: &ParsedNodeFn, + regular_fields: &[&ParsedField], field_names: &[&Ident], struct_name: &Ident, mapped_struct_name: &Ident, @@ -1170,7 +1305,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 @@ -1206,23 +1340,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 { @@ -1237,7 +1367,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>))) @@ -1258,17 +1388,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>)); + 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() { true => parsed.input.ty.clone(), @@ -1282,19 +1407,14 @@ 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 - + NodeIO::<#input_type>::to_node_io(&node, params) } ) )); diff --git a/node-graph/node-macro/src/parsing.rs b/node-graph/node-macro/src/parsing.rs index 16da13e276c..069a33641b4 100644 --- a/node-graph/node-macro/src/parsing.rs +++ b/node-graph/node-macro/src/parsing.rs @@ -59,6 +59,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, /// Set on the `Destructure` derive's generated extractor nodes, which take the struct rather than an `Item` or `List`. Not parsed from source. pub(crate) destructure_extractor: bool, } @@ -69,6 +73,7 @@ pub enum ParsedValueSource { None, Default(TokenStream2), Scope(Box), + SourceId, } // #[widget(ParsedWidgetOverride::Hidden)] @@ -128,6 +133,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 { @@ -135,7 +174,7 @@ impl ParsedField { || matches!( self.ty.regular(), Some(RegularParsedField { - value_source: ParsedValueSource::Scope(_), + value_source: ParsedValueSource::Scope(_) | ParsedValueSource::SourceId, .. }) ) @@ -417,6 +456,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; @@ -571,13 +612,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', 'icon', 'path', 'skip_impl', 'inspector', 'cfg', 'shader_node', 'serialize', 'memoize', and 'inject_scope'. + Supported attributes are 'category', 'name', 'icon', 'path', 'skip_impl', 'inspector', 'cfg', 'shader_node', 'serialize', 'memoize', 'inject_scope', 'placeholder', and 'no_partial'. Example usage: #[node_macro::node(..., name("Test Node"), ...)] "# @@ -612,6 +677,8 @@ impl Parse for NodeFnAttributes { serialize, memoize, inject_scope, + placeholder, + no_partial, destructure_extractor: false, }) } @@ -633,7 +700,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 @@ -1165,7 +1232,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(), @@ -1185,6 +1252,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::*; @@ -1308,6 +1417,8 @@ mod tests { serialize: None, memoize: false, inject_scope: false, + placeholder: None, + no_partial: false, destructure_extractor: false, }, fn_name: Ident::new("add", Span::call_site()), @@ -1384,6 +1495,8 @@ mod tests { serialize: None, memoize: false, inject_scope: false, + placeholder: None, + no_partial: false, destructure_extractor: false, }, fn_name: Ident::new("transform", Span::call_site()), @@ -1477,6 +1590,8 @@ mod tests { serialize: None, memoize: false, inject_scope: false, + placeholder: None, + no_partial: false, destructure_extractor: false, }, fn_name: Ident::new("circle", Span::call_site()), @@ -1549,6 +1664,8 @@ mod tests { serialize: None, memoize: false, inject_scope: false, + placeholder: None, + no_partial: false, destructure_extractor: false, }, fn_name: Ident::new("levels", Span::call_site()), @@ -1633,6 +1750,8 @@ mod tests { serialize: None, memoize: false, inject_scope: false, + placeholder: None, + no_partial: false, destructure_extractor: false, }, fn_name: Ident::new("add", Span::call_site()), @@ -1720,6 +1839,8 @@ mod tests { serialize: None, memoize: false, inject_scope: false, + placeholder: None, + no_partial: false, destructure_extractor: false, }, fn_name: Ident::new("load_image", Span::call_site()), @@ -1792,6 +1913,8 @@ mod tests { serialize: None, memoize: false, inject_scope: false, + placeholder: None, + no_partial: false, destructure_extractor: false, }, fn_name: Ident::new("custom_node", Span::call_site()), @@ -1969,4 +2092,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/shader_nodes/per_pixel_adjust.rs b/node-graph/node-macro/src/shader_nodes/per_pixel_adjust.rs index 2805d769883..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, @@ -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) } }; @@ -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(), @@ -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/node-macro/src/validation.rs b/node-graph/node-macro/src/validation.rs index 4e6a91ab21b..225fd73a280 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; @@ -72,9 +72,16 @@ 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; } + 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 +95,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 +115,7 @@ fn validate_element_wise(parsed: &ParsedNodeFn) { ); } - let output_type = &parsed.output_type; + let output_type = &output; let ranked = outer_wrapper_is(output_type, "Item") || outer_wrapper_is(output_type, "List"); if !ranked && (outer_wrapper_is(output_type, "ListDyn") || is_unit_type(output_type) || contains_generic_param(output_type, &parsed.fn_generics)) { emit_error!( diff --git a/node-graph/nodes/brush/src/basic_brush/mod.rs b/node-graph/nodes/brush/src/basic_brush/mod.rs index 6686e9be363..0141996005b 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"), icon("NodeBasicBrush"))] -pub async fn basic_brush<'a: 'n>( +fn basic_brush( ctx: impl Ctx + ExtractFootprint, strokes: List, #[widget(ParsedWidgetOverride::Hidden)] _cache: Item, @@ -57,7 +57,7 @@ pub 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); @@ -65,9 +65,9 @@ pub async fn basic_brush<'a: 'n>( } #[node_macro::node(category(""), inject_scope)] -async 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/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/gcore/src/animation.rs b/node-graph/nodes/gcore/src/animation.rs index fbb7c30e14f..d736f418c4d 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 graphene_animation::AnimationCurve; use graphic_types::vector_types::Gradient; @@ -73,7 +73,7 @@ fn animation_time( } #[node_macro::node(category("Debug"), icon("NodeQuantizeRealTime"))] -async fn quantize_real_time( +fn quantize_real_time( ctx: impl Ctx + ExtractAll + CloneVarArgs, #[implementations( Context -> Item, @@ -101,11 +101,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(); @@ -115,11 +115,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"), icon("NodeQuantizeAnimationTime"))] -async fn quantize_animation_time( +fn quantize_animation_time( ctx: impl Ctx + ExtractAll + CloneVarArgs, #[implementations( Context -> Item, @@ -147,11 +147,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(); @@ -159,7 +159,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.rs b/node-graph/nodes/gcore/src/context.rs index eb28a53e9e4..66576ad8d34 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"), icon("NodeReadPosition"), 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"), icon("NodeReadIndex"), 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/gcore/src/context_modification.rs b/node-graph/nodes/gcore/src/context_modification.rs index 8862bba3938..d98e54646d5 100644 --- a/node-graph/nodes/gcore/src/context_modification.rs +++ b/node-graph/nodes/gcore/src/context_modification.rs @@ -1,8 +1,8 @@ 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}; +use core_types::{Color, Interrupt, OwnedContextImpl}; use glam::{DAffine2, DVec2}; use graphene_animation::AnimationCurve; use graphic_types::vector_types::Gradient; @@ -12,7 +12,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("_Context Nullification"))] -async fn context_modification( +fn context_modification( ctx: impl Ctx + CloneVarArgs + ExtractAll, /// The data to pass through, evaluated with the stripped down context. #[implementations( @@ -48,16 +48,18 @@ 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, -) -> T { - let new_context = OwnedContextImpl::from_flags(ctx, features_to_keep.into_element()); + modification: Item, +) -> 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)] mod tests { use super::*; + use core_types::SourceId; + use core_types::context::ContextFeatures; use core_types::graphene_hash::CacheHash; use core_types::transform::Footprint; use std::collections::hash_map::DefaultHasher; @@ -122,4 +124,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)])); + } } diff --git a/node-graph/nodes/gcore/src/memo.rs b/node-graph/nodes/gcore/src/memo.rs index a7756b37ec5..ced5c703097 100644 --- a/node-graph/nodes/gcore/src/memo.rs +++ b/node-graph/nodes/gcore/src/memo.rs @@ -1,7 +1,7 @@ use core_types::graphene_hash::CacheHash; use core_types::memo::*; use core_types::registry::{DowncastBothNode, DynAnyNode, NODE_REGISTRY, NodeConstructor, PanicNode, TypeErasedBox}; -use core_types::{Context, NodeIO, NodeIOTypes, WasmNotSend, concrete, fn_type_fut, future}; +use core_types::{Context, Interrupt, NodeIO, NodeIOTypes, Poll, WasmNotSend, concrete, fn_type}; use dyn_any::StaticType; use std::hash::DefaultHasher; use std::hash::Hasher; @@ -12,7 +12,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"), icon("NodeMemoize"), 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. @@ -29,25 +29,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::Pending | Poll::Error(_)) { + *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), inspector("monitor_inspector"), 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> { @@ -58,29 +60,25 @@ fn serialize_monitor + 'static>() { let memoize_constructor: NodeConstructor = |mut arguments| { - Box::pin(async move { - let node = MemoizeNode::new(DowncastBothNode::::new(arguments.pop().expect("Not enough arguments provided to construct node"))); - let any: DynAnyNode = DynAnyNode::new(node); - Box::new(any) as TypeErasedBox - }) + let node = MemoizeNode::new(DowncastBothNode::::new(arguments.pop().expect("Not enough arguments provided to construct node"))); + let any: DynAnyNode = DynAnyNode::new(node); + Box::new(any) as TypeErasedBox }; let monitor_constructor: NodeConstructor = |mut arguments| { - Box::pin(async move { - let node = MonitorNode::new(DowncastBothNode::::new(arguments.pop().expect("Not enough arguments provided to construct node"))); - let any: DynAnyNode = DynAnyNode::new(node); - Box::new(any) as TypeErasedBox - }) + let node = MonitorNode::new(DowncastBothNode::::new(arguments.pop().expect("Not enough arguments provided to construct node"))); + let any: DynAnyNode = DynAnyNode::new(node); + Box::new(any) as TypeErasedBox }; let memoize_types: NodeIOTypes = { - let node = MemoizeNode::new(PanicNode:: + Send>>>::new()); - let mut node_io = NodeIO::<'_, Context>::to_async_node_io(&node, vec![fn_type_fut!(Context, T)]); + let node = MemoizeNode::new(PanicNode::::new()); + let mut node_io = NodeIO::::to_node_io(&node, vec![fn_type!(Context, T)]); node_io.call_argument = concrete!(::Static); node_io }; let monitor_types: NodeIOTypes = { - let node = MonitorNode::new(PanicNode:: + Send>>>::new()); - let mut node_io = NodeIO::<'_, Context>::to_async_node_io(&node, vec![fn_type_fut!(Context, T)]); + let node = MonitorNode::new(PanicNode::::new()); + let mut node_io = NodeIO::::to_node_io(&node, vec![fn_type!(Context, T)]); node_io.call_argument = concrete!(::Static); node_io }; @@ -89,3 +87,46 @@ pub fn register_memoize_and_monitor 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]); + } + + struct StandIn(Arc); + + 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); + } +} diff --git a/node-graph/nodes/gcore/src/ops.rs b/node-graph/nodes/gcore/src/ops.rs index 1af9e0bdf4b..e840aeb14b3 100644 --- a/node-graph/nodes/gcore/src/ops.rs +++ b/node-graph/nodes/gcore/src/ops.rs @@ -92,22 +92,22 @@ 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("_Adapters"), 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(); - 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. #[node_macro::node(category("_Adapters"), 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(); 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/graphic/src/artboard.rs b/node-graph/nodes/graphic/src/artboard.rs index 4f20a023c3d..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(""))] -pub async fn create_artboard( +fn create_artboard( ctx: impl ExtractAll + CloneVarArgs + Ctx, /// Graphics to include within the artboard. #[implementations( @@ -34,7 +34,7 @@ pub 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 @@ pub 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 @@ pub 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 dbc95140e77..e0302846850 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}; @@ -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"), icon("NodeRemoveAtIndex"), 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"), icon("NodeItemAtIndex"), 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( @@ -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"), icon("NodeMirror"))] -async fn mirror( +fn mirror( _: impl Ctx, #[implementations( Graphic, @@ -503,7 +503,7 @@ async 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())) @@ -516,7 +516,7 @@ pub 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"), icon("NodeWriteAttribute"))] -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"), icon("NodeExtend"))] -pub async 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 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( +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"), icon("NodeIntoGroup"))] -pub async fn into_group + 'n>( +fn into_group>( _: 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"), icon("NodeAsGraphic"))] -pub async 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"), icon("NodeFlattenGraphic"))] -pub async 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? @@ -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"), icon("NodeFlattenVector"))] -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"), icon("NodeFlattenRaster"))] -pub async 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"), icon("NodeFlattenColor"))] -pub async 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"), icon("NodeFlattenGradient"))] -pub async 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/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/lib.rs b/node-graph/nodes/gstd/src/lib.rs index e75853f9a6f..90705411676 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/platform_application_io.rs b/node-graph/nodes/gstd/src/platform_application_io.rs index 13f81dbcff8..9009d93928d 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}; @@ -40,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"), icon("NodeGetRequest"))] +#[node_macro::node(category("Web Request"), icon("NodeGetRequest"), placeholder = pending_text)] async fn get_request( _: impl Ctx, _primary: (), @@ -58,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(); } @@ -75,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"), icon("NodePostRequest"))] +#[node_macro::node(category("Web Request"), icon("NodePostRequest"), placeholder = pending_post_text)] async fn post_request( _: impl Ctx, _primary: (), @@ -98,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(); } @@ -115,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"), icon("NodeStringToBytes"), name("String to Bytes"))] fn string_to_bytes(_: impl Ctx, string: Item) -> Item { @@ -138,8 +133,8 @@ 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"), icon("NodeLoadResource"))] -async fn load_resource<'a: 'n>(_: impl Ctx, _primary: (), #[name("URL")] url: Item) -> Item { +#[node_macro::node(category("Web Request"), icon("NodeLoadResource"), 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()) }; @@ -160,6 +155,10 @@ async fn load_resource<'a: 'n>(_: impl Ctx, _primary: (), #[name("URL")] url: It } } +fn pending_resource(_primary: &(), _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. @@ -173,13 +172,13 @@ 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()) } /// 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(); @@ -238,24 +237,30 @@ 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)] -pub async 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(""))] -pub async fn resource<'a: 'n>( +#[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. #[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(); }; @@ -268,29 +273,34 @@ pub async fn resource<'a: 'n>( 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)] -pub async 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)] -pub async 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"), icon("NodeUploadTexture"), memoize)] -pub async 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(); - Item::from_parts(raster.convert(Footprint::DEFAULT, executor).await, attributes) + Item::from_parts(raster.convert(Footprint::DEFAULT, executor), attributes) } 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; +} diff --git a/node-graph/nodes/gstd/src/render_background.rs b/node-graph/nodes/gstd/src/render_background.rs index ae60d18974c..c4d2f493f6b 100644 --- a/node-graph/nodes/gstd/src/render_background.rs +++ b/node-graph/nodes/gstd/src/render_background.rs @@ -9,10 +9,10 @@ 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>( +fn render_background( ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, #[scope(composite_background_pipeline::IDENTIFIER)] pipeline: Item, data: Item, @@ -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.unwrap_or(1.).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.unwrap_or(1.).to_f32(), + }); RenderOutputType::Texture(blended) } @@ -122,9 +119,9 @@ async fn render_background<'a: 'n>( } #[node_macro::node(category(""), inject_scope)] -async 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() { @@ -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_cache.rs b/node-graph/nodes/gstd/src/render_cache.rs index a87c5689d2e..6e520565048 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, 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; @@ -27,10 +26,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, } @@ -49,6 +47,7 @@ pub struct CacheKey { pub animation_time_ms: i64, pub real_time_ms: i64, pub pointer: [u8; 16], + pub generations: Generations, } impl CacheKey { @@ -66,6 +65,7 @@ impl CacheKey { animation_time: f64, real_time: f64, pointer: Option, + generations: Generations, ) -> Self { let pointer_bytes = pointer .map(|p| { @@ -91,6 +91,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, } } } @@ -323,18 +324,18 @@ fn flood_fill(start: &TileCoord, tile_set: &HashSet, visited: &mut Ha } #[node_macro::node(category(""))] -pub async fn render_output_cache<'a: 'n>( +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 { +) -> Poll> { let footprint = ctx.footprint(); let Some(mut render_params) = ctx.vararg(0).ok().and_then(|v| v.downcast_ref::().cloned()) 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()); }; render_params.viewport_zoom = if let RenderMode::Outline = render_params.render_mode { render_params.viewport_zoom } else { None }; @@ -343,7 +344,7 @@ pub async fn render_output_cache<'a: 'n>( 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; @@ -370,41 +371,53 @@ pub async fn render_output_cache<'a: 'n>( 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); + // 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).await; - 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()).await; - } + 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); + let combined_metadata = composite_cached_regions(&all_regions, &output_texture, &device_origin_offset, &footprint.transform, &executor); - 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), + } } -async fn render_missing_region( +fn render_missing_region( region: &RenderRegion, render_fn: F, ctx: impl Ctx + ExtractAll + CloneVarArgs, @@ -413,8 +426,7 @@ async fn render_missing_region( viewport_origin_offset: &DVec2, ) -> CachedRegion where - F: Fn(Context<'static>) -> Fut, - Fut: std::future::Future>, + 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,24 +444,18 @@ 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 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; + let output = render_fn(region_ctx).map(|output| { + let mut output = output.into_element(); + output.metadata.apply_transform(region_transform.inverse()); + output + }); CachedRegion { - texture, - texture_size: region_pixel_size, + 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, } } @@ -468,29 +474,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 +524,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); } diff --git a/node-graph/nodes/gstd/src/render_node.rs b/node-graph/nodes/gstd/src/render_node.rs index 094041fb41a..b84476e9428 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<'a: 'n, T: 'static + Render + WasmNotSend + Send + Sync>( +fn render_intermediate( ctx: impl Ctx + ExtractVarArgs + ExtractAll + CloneVarArgs, #[implementations( Context -> List, @@ -35,7 +35,7 @@ async fn render_intermediate<'a: 'n, T: 'static + Render + WasmNotSend + Send + 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<'a: 'n, T: 'static + Render + WasmNotSend + Send + .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,13 +72,13 @@ async fn render_intermediate<'a: 'n, T: 'static + Render + WasmNotSend + Send + } }; - Item::new_from_element(intermediate) + Ok(Item::new_from_element(intermediate)) } #[node_macro::node(category(""))] -async 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(); @@ -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) } @@ -147,11 +146,11 @@ async fn render<'a: 'n>( } #[node_macro::node(category(""))] -async fn create_context<'a: 'n>( +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() @@ -183,10 +182,11 @@ async fn create_context<'a: 'n>( .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; + 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 eb1546b0227..7f4bc7cc0ac 100644 --- a/node-graph/nodes/gstd/src/render_pixel_preview.rs +++ b/node-graph/nodes/gstd/src/render_pixel_preview.rs @@ -1,23 +1,23 @@ 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; 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(""))] -pub async fn render_pixel_preview<'a: 'n>( +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 @@ pub async fn render_pixel_preview<'a: 'n>( 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,35 +53,32 @@ pub async fn render_pixel_preview<'a: 'n>( }; 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; 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); 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)] -async 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() { @@ -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; diff --git a/node-graph/nodes/math/src/lib.rs b/node-graph/nodes/math/src/lib.rs index dcea6069b72..410937fda92 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}; @@ -1377,7 +1377,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"), icon("NodeSwitch"))] -async fn switch( +fn switch( ctx: impl Ctx + CloneVarArgs + ExtractAll, condition: Item, #[expose] @@ -1440,10 +1440,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. @@ -2327,8 +2327,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/path-bool/src/lib.rs b/node-graph/nodes/path-bool/src/lib.rs index 3af6a11db5b..5ec2555862c 100644 --- a/node-graph/nodes/path-bool/src/lib.rs +++ b/node-graph/nodes/path-bool/src/lib.rs @@ -19,7 +19,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"), icon("NodeBooleanOperation"), 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, diff --git a/node-graph/nodes/raster/src/adjustments.rs b/node-graph/nodes/raster/src/adjustments.rs index 849492427fd..6b06a5838a3 100644 --- a/node-graph/nodes/raster/src/adjustments.rs +++ b/node-graph/nodes/raster/src/adjustments.rs @@ -679,7 +679,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"), icon("NodeCurves"), inspector("transfer_curves_inspector"))] -async fn curves + Send>( +fn curves + Send>( _: impl Ctx, #[implementations(Raster, Color, Gradient)] image: Item, curve: Item, @@ -1466,7 +1466,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"), icon("NodeGradientMap"))] -async fn gradient_map + Send>( +fn gradient_map + Send>( _: impl Ctx, #[implementations(Raster, Color, Gradient)] image: Item, #[default(Color::BLACK, Color::WHITE)] gradient: Item, @@ -2537,7 +2537,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"), icon("NodeColorLookup"))] -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/blending_nodes.rs b/node-graph/nodes/raster/src/blending_nodes.rs index 81c48095665..6562d33aaa4 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/raster/src/dehaze.rs b/node-graph/nodes/raster/src/dehaze.rs index 604fd70d25b..7b79b096891 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"), icon("NodeDehaze"))] -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 44cf030d9d6..91d0cafe04a 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"), icon("NodeBlur"))] -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"), icon("NodeMedianFilter"))] -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 f7d3e9989e8..f569a7cff64 100644 --- a/node-graph/nodes/raster/src/image_color_palette.rs +++ b/node-graph/nodes/raster/src/image_color_palette.rs @@ -15,7 +15,7 @@ const OKLAB_GAMUT_RANGES: [(f32, f32); 3] = [(0., 1.), (-0.24, 0.28), (-0.32, 0. /// Finds the colors that best represent the distinct color ranges in an image, ignoring transparent areas. #[node_macro::node(category("Color"), icon("NodeImageColorPalette"))] -async fn image_color_palette( +fn image_color_palette( _: impl Ctx, /// The image to find colors in. image: Item>, @@ -236,7 +236,7 @@ mod test { base64_string: None, }); - let palette = futures::executor::block_on(image_color_palette((), Item::new_from_element(image), Item::new_from_element(count))); + let palette = image_color_palette((), Item::new_from_element(image), Item::new_from_element(count)); palette.iter_element_values().copied().collect() } diff --git a/node-graph/nodes/raster/src/std_nodes.rs b/node-graph/nodes/raster/src/std_nodes.rs index 83040a76f25..2dfdeff73a6 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"), icon("NodeSampleImage"))] -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"), icon("NodeChannelsToImage"), name("Channels to Image"))] -pub fn channels_to_image( +fn channels_to_image( _: impl Ctx, _primary: (), #[expose] red: Item>, @@ -150,7 +150,7 @@ pub fn channels_to_image( } #[node_macro::node(category("Raster"), icon("NodeMask"))] -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"), icon("NodeEmptyImage"))] -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"), icon("NodeImage"))] -pub fn image<'a: 'n>( +fn image( _: 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"), icon("NodeNoisePattern"))] #[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"), icon("NodeMandelbrot"))] -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 29bec64ceee..f6e6112134a 100644 --- a/node-graph/nodes/repeat/src/repeat_nodes.rs +++ b/node-graph/nodes/repeat/src/repeat_nodes.rs @@ -1,7 +1,7 @@ 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}; @@ -9,7 +9,7 @@ use vector_types::Gradient; use vector_types::vector::{no_paths, paths_from_list_row_attributes}; #[node_macro::node(category("Repeat"), icon("NodeRepeat"))] -async fn repeat( +fn repeat( ctx: impl ExtractAll + CloneVarArgs + Ctx, #[implementations( Context -> List, @@ -26,12 +26,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()); @@ -43,18 +43,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"), icon("NodeRepeatArray"))] -pub async fn repeat_array( +fn repeat_array( ctx: impl ExtractAll + CloneVarArgs + Ctx, #[implementations( Context -> List, @@ -71,7 +71,7 @@ pub 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 Inspector layout in document_node_definitions.rs and this default is set, the widget weirdly doesn't show up in the Inspector. Investigation is needed. @@ -83,7 +83,7 @@ pub 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 @@ -97,7 +97,7 @@ pub 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 }; @@ -111,11 +111,11 @@ pub async fn repeat_array( } } - result_list + Ok(result_list) } #[node_macro::node(category("Repeat"), icon("NodeRepeatRadial"))] -async fn repeat_radial( +fn repeat_radial( ctx: impl ExtractAll + CloneVarArgs + Ctx, #[implementations( Context -> List, @@ -132,7 +132,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)] @@ -143,7 +143,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(); @@ -154,7 +154,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 }; @@ -168,11 +168,11 @@ async fn repeat_radial( } } - result_list + Ok(result_list) } #[node_macro::node(category("Repeat"), icon("NodeRepeatOnPoints"), name("Repeat on Points"))] -async fn repeat_on_points( +fn repeat_on_points( ctx: impl ExtractAll + CloneVarArgs + Sync + Ctx + InjectVarArgs, points: List, #[implementations( @@ -190,9 +190,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(); @@ -201,16 +201,17 @@ 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 paths = paths_from_list_row_attributes(&points, points_index).unwrap_or_else(|| no_paths()); @@ -218,32 +219,29 @@ async fn repeat_on_points( let range = 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 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::Path; use vector_types::vector::misc::BoxCorners; @@ -258,13 +256,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()) } } @@ -272,14 +269,10 @@ 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) } } @@ -287,27 +280,26 @@ mod test { #[derive(Clone)] struct ExtractYNode(Position); - impl<'i, I: Ctx, Position> Node<'i, I> for ExtractYNode + impl Node for ExtractYNode where - Position: Node<'i, I, Output = Pin> + 'i + Send>>>, + Position: Node>, { - type Output = Pin> + 'i + Send>>; - fn eval(&'i self, input: I) -> Self::Output { - let position = self.0.eval(input); - Box::pin(async move { Item::new_from_element(position.await.element().y) }) + type Output = Item; + fn eval(&self, input: &I) -> Poll> { + self.0.eval(input).map(|position| Item::new_from_element(position.element().y)) } } - #[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(()), - ExtractYNode(ReadPositionNode::new(FutureWrapperNode(()), FutureWrapperNode(Item::new_from_element(0_i64)))), - 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(()), + ExtractYNode(ReadPositionNode::new(ValueNode(()), ValueNode(Item::new_from_element(0_i64)))), + 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.)]; @@ -317,7 +309,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 @@ -330,20 +323,21 @@ 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; - let vector_list = List::new_from_item(vector_nodes::combine_paths(Footprint::default(), List::new_from_element(Graphic::VectorList(repeated))).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(); let paths = row_paths(&vector_list, 0); assert_eq!(vector.stroke_manipulator_groups(paths).count(), 3); @@ -352,18 +346,19 @@ 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; - let vector_list = List::new_from_item(vector_nodes::combine_paths(Footprint::default(), List::new_from_element(Graphic::VectorList(repeated))).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(); let paths = row_paths(&vector_list, 0); assert_eq!(vector.stroke_manipulator_groups(paths).count(), 1); @@ -373,20 +368,21 @@ 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; - let vector_list = List::new_from_item(vector_nodes::combine_paths(Footprint::default(), List::new_from_element(Graphic::VectorList(repeated))).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(); let paths = row_paths(&vector_list, 0); assert_eq!(vector.stroke_manipulator_groups(paths).count(), 8); @@ -395,18 +391,19 @@ 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; - let vector_list = List::new_from_item(vector_nodes::combine_paths(Footprint::default(), List::new_from_element(Graphic::VectorList(repeated))).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(); let paths = row_paths(&vector_list, 0); assert_eq!(vector.stroke_manipulator_groups(paths).count(), 8); diff --git a/node-graph/nodes/text/src/lib.rs b/node-graph/nodes/text/src/lib.rs index ddf6fcd4e00..a9b4c9d05f0 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; @@ -948,24 +949,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"), icon("NodeMapString"))] -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 6200825002d..40b05a40060 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"), icon("NodeTransform"))] -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"), icon("NodeExtractTransform"), 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 c25257d99f9..514e364d663 100644 --- a/node-graph/nodes/vector/src/vector_modification_nodes.rs +++ b/node-graph/nodes/vector/src/vector_modification_nodes.rs @@ -9,7 +9,7 @@ use vector_types::vector::{VectorModification, edit_item_geometry, ensure_paths_ /// 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; let modification = modification.into_element(); @@ -54,7 +54,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); @@ -70,8 +70,8 @@ mod tests { use vector_types::vector::{CurveType, PointId, SegmentId, VectorModificationType, paths_from_item_attributes, push_path}; // A lone loop fills by its contour alone, so it keeps the direction it was drawn in even when that winds backwards - #[tokio::test] - async fn a_drawn_loop_keeps_its_direction() { + #[test] + fn a_drawn_loop_keeps_its_direction() { let mut bezpath = BezPath::new(); bezpath.move_to((0., 0.)); bezpath.line_to((0., 10.)); @@ -80,14 +80,14 @@ mod tests { bezpath.close_path(); let drawn = Vector::item_from_bezpath(&bezpath); - let modified = path_modify((), drawn.clone(), Item::default(), Item::default()).await; + let modified = path_modify((), drawn.clone(), Item::default(), Item::default()); assert_eq!(paths_from_item_attributes(&modified), paths_from_item_attributes(&drawn)); } // Moving a point of a mesh leaves its paths in place to begin with, and its loops still get their winding set for the face fill - #[tokio::test] - async fn a_mesh_moved_in_place_still_has_its_winding_set() { + #[test] + fn a_mesh_moved_in_place_still_has_its_winding_set() { // A reverse-wound square beside three spokes meeting at a hub, which makes the geometry branch let positions = [(0., 0.), (0., 10.), (10., 10.), (10., 0.), (30., 0.), (40., 0.), (30., 10.), (20., 10.)]; let (mut vector, mut paths) = (Vector::default(), List::new()); @@ -105,7 +105,7 @@ mod tests { let mut modification = VectorModification::default(); modification.modify(&VectorModificationType::ApplyPointDelta { point: hub, delta: DVec2::X }); - let modified = path_modify((), drawn.clone(), Item::new_from_element(Box::new(modification)), Item::default()).await; + let modified = path_modify((), drawn.clone(), Item::new_from_element(Box::new(modification)), Item::default()); let first_path = |item: &Item| paths_from_item_attributes(item).unwrap().element(0).unwrap().0.iter_element_values().copied().collect::>(); assert_eq!(first_path(&drawn), [0, 1, 2, 3, 0]); @@ -113,8 +113,8 @@ mod tests { } // Moving points leaves the paths shared with the incoming geometry, and where they sat among its attributes - #[tokio::test] - async fn moving_points_leaves_the_paths_shared_and_in_place() { + #[test] + fn moving_points_leaves_the_paths_shared_and_in_place() { let mut bezpath = BezPath::new(); bezpath.move_to((0., 0.)); bezpath.line_to((10., 0.)); @@ -124,7 +124,7 @@ mod tests { let mut modification = VectorModification::default(); modification.modify(&VectorModificationType::ApplyPointDelta { point: corner, delta: DVec2::X }); - let modified = path_modify((), drawn.clone(), Item::new_from_element(Box::new(modification)), Item::default()).await; + let modified = path_modify((), drawn.clone(), Item::new_from_element(Box::new(modification)), Item::default()); assert_eq!(modified.element().0.element(1), Some(&DVec2::new(11., 0.))); assert!(std::ptr::eq(paths_from_item_attributes(&drawn).unwrap(), paths_from_item_attributes(&modified).unwrap())); @@ -132,8 +132,8 @@ mod tests { } // A point cloud comes out with an empty paths attribute placed last, whether the edit is made in place or by reweaving - #[tokio::test] - async fn a_point_cloud_gains_an_empty_paths_attribute() { + #[test] + fn a_point_cloud_gains_an_empty_paths_attribute() { let mut cloud = Vector::default(); cloud.0.push(Item::new_from_element(DVec2::ZERO).with_attribute(ATTR_ID, PointId::ZERO)); let drawn = Item::new_from_element(cloud).with_attribute(ATTR_TRANSFORM, DAffine2::IDENTITY); @@ -145,7 +145,7 @@ mod tests { }); for modification in [VectorModification::default(), adds_a_point] { - let modified = path_modify((), drawn.clone(), Item::new_from_element(Box::new(modification)), Item::default()).await; + let modified = path_modify((), drawn.clone(), Item::new_from_element(Box::new(modification)), Item::default()); assert_eq!(paths_from_item_attributes(&modified).map(List::len), Some(0)); assert_eq!(modified.attributes().keys().collect::>(), [ATTR_TRANSFORM, ATTR_PATHS, ATTR_EDITOR_LAYER_PATH]); diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index 4c50ed81a1d..016116766ea 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -1,6 +1,7 @@ use core::cmp::Ordering; use core::f64::consts::{PI, TAU}; use core::hash::{Hash, Hasher}; +use core_types::Interrupt; use core_types::blending::BlendMode; use core_types::bounds::{BoundingBox, RenderBoundingBox}; use core_types::list::{ATTR_APPEARANCE, ATTR_ID, ATTR_PATHS, Item, ItemAttributeValues, List, ListDyn, NodeIdPath}; @@ -263,7 +264,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"), icon("NodeAssignColors"), 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)] @@ -292,7 +293,7 @@ async 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()); @@ -352,7 +353,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"), icon("NodeFill"), path(graphene_core::vector), inspector("fill_inspector"))] -async fn fill( +fn fill( _: impl Ctx, /// The content with vector paths to apply the fill style to. #[implementations(Vector, Graphic)] @@ -365,7 +366,7 @@ async 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()); @@ -442,7 +443,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"), icon("NodeStroke"), path(graphene_core::vector), inspector("stroke_inspector"))] -async fn stroke( +fn stroke( _: impl Ctx, /// The content with vector paths to apply the stroke style to. #[implementations(Vector, Graphic)] @@ -472,7 +473,7 @@ async 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) = ( @@ -521,7 +522,7 @@ fn dash_pattern( } #[node_macro::node(category("Repeat"), icon("NodeRepeatOnPoints"), 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. @@ -612,7 +613,7 @@ async fn copy_to_points( } #[node_macro::node(category("Vector: Modifier"), icon("NodeRoundCorners"), path(core_types::vector))] -async fn round_corners( +fn round_corners( _: impl Ctx, #[implementations(Graphic, Vector)] source: Item, #[unit(" px")] @@ -1022,7 +1023,7 @@ pub mod extrude_algorithms { } #[node_macro::node(category("Vector: Modifier"), icon("NodeExtrude"), 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()); @@ -1033,7 +1034,7 @@ async fn extrude(_: impl Ctx, #[implementations(G } #[node_macro::node(category("Vector: Modifier"), icon("NodeBoxWarp"), 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_paths = shared_paths_from_item_attributes(&rectangle); let target = rectangle.into_element(); @@ -1104,7 +1105,7 @@ fn bilinear_interpolate(t: DVec2, quad: &[DVec2; 4]) -> DVec2 { } #[node_macro::node(category("Vector"), icon("NodePackStrips"), path(graphene_core::vector))] -async fn pack_strips( +fn pack_strips( _: impl Ctx, #[implementations( List, @@ -1227,7 +1228,7 @@ where /// Automatically constructs tangents (Bézier handles) for anchor points in a vector path. #[node_macro::node(category("Vector: Modifier"), icon("NodeAutoTangents"), 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). @@ -1398,7 +1399,7 @@ async fn auto_tangents( } #[node_macro::node(category("Vector: Modifier"), icon("NodeBoundingBox"), 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 paths = shared_paths_from_item_attributes(&content); @@ -1415,7 +1416,7 @@ async fn bounding_box(_: impl Ctx, #[implementati } #[node_macro::node(category("Vector: Measure"), icon("NodeDimensions"), path(core_types::vector))] -async fn dimensions(_: impl Ctx, content: Item) -> Item { +fn dimensions(_: impl Ctx, content: Item) -> Item { let paths = paths_from_item_attributes(&content).unwrap_or_else(|| no_paths()); let dimensions = content .element() @@ -1450,7 +1451,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"), icon("NodePointsToPolyline"), 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| { @@ -1482,7 +1483,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"), icon("NodeRelaxPoints"), path(core_types::vector))] -async fn relax_points( +fn relax_points( _: impl Ctx, /// A vector path or point cloud to relax. #[implementations(Graphic, Vector)] @@ -1515,7 +1516,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"), icon("NodeVoronoiCells"), 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(); @@ -1536,7 +1537,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"), icon("NodeTriangulate"), 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(); @@ -1646,7 +1647,7 @@ fn mesh_weld_tolerance(polygons: &[Vec]) -> f64 { } #[node_macro::node(category("Vector: Modifier"), icon("NodeOffsetPath"), path(core_types::vector), inspector("offset_path_inspector"))] -async fn offset_path( +fn offset_path( _: impl Ctx, #[implementations(Graphic, Vector)] content: Item, distance: Item, @@ -1857,14 +1858,14 @@ fn solidify_stroke_list(content: List) -> List { } #[node_macro::node(category("Vector: Modifier"), icon("NodeSolidifyStroke"), 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"), icon("NodeSeparateSubpaths"), 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 content_paths = shared_paths_from_item_attributes(&content); let bezpaths = content.element().stroke_bezpath_iter(&content_paths).collect::>(); @@ -1892,7 +1893,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"), icon("NodePathIsClosed"), path(core_types::vector))] -async fn path_is_closed( +fn path_is_closed( _: impl Ctx, /// The vector content whose subpaths are inspected. content: Item, @@ -1908,11 +1909,11 @@ async fn path_is_closed( } #[node_macro::node(category("Vector"), icon("NodeMapPoints"), 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; // Handles keep their own positions, so the mapping walks anchors alone, indexed in anchor order @@ -1921,19 +1922,19 @@ async fn map_points( let Some(position) = vector.position(pool_index) else { continue }; let owned_ctx = OwnedContextImpl::from(ctx.clone()).with_index(index).with_position(position); - let mapped_position = mapped.eval(owned_ctx.into_context()).await.into_element(); + let mapped_position = mapped.eval(owned_ctx.into_context())?.into_element(); if let Some(slot) = vector.0.element_mut(pool_index) { *slot = mapped_position; } } } - content + Ok(content) } /// Combines every vector path across the input into a single compound path. #[node_macro::node(category("Vector"), icon("NodeCombinePaths"), 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::(); @@ -1979,7 +1980,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"), icon("NodeSamplePolyline"), path(core_types::vector), inspector("sample_polyline_inspector"), memoize)] -async fn sample_polyline( +fn sample_polyline( _: impl Ctx, #[implementations(Graphic, Vector)] content: Item, spacing: Item, @@ -2063,7 +2064,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"), icon("NodeSimplify"), path(core_types::vector))] -async fn simplify( +fn simplify( _: impl Ctx, /// The vector paths to simplify. #[implementations(Graphic, Vector)] @@ -2108,7 +2109,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"), icon("NodeDecimate"), path(core_types::vector))] -async fn decimate( +fn decimate( _: impl Ctx, /// The vector paths to decimate. #[implementations(Graphic, Vector)] @@ -2240,7 +2241,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"), icon("NodeCutPath"), path(graphene_core::vector))] -async fn cut_path( +fn cut_path( _: impl Ctx, /// The path to insert a cut into. #[implementations(Graphic, Vector)] @@ -2292,7 +2293,7 @@ async fn cut_path( /// Cuts path segments into separate disconnected pieces where each is a distinct subpath. #[node_macro::node(category("Vector: Modifier"), icon("NodeCutSegments"), 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 @@ -2374,7 +2375,7 @@ pub struct PathEvaluation { /// /// 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: Measure"), icon("NodeEvaluatePath"), path(graphene_core::vector))] -async fn evaluate_path( +fn evaluate_path( _: impl Ctx, /// The path to traverse. content: Item, @@ -2431,7 +2432,7 @@ async fn evaluate_path( } #[node_macro::node(category("Vector: Modifier"), icon("NodeScatterPoints"), path(core_types::vector), memoize)] -async fn scatter_points( +fn scatter_points( _: impl Ctx, #[implementations(Graphic, Vector)] content: Item, #[unit(" px")] @@ -2484,7 +2485,7 @@ async fn scatter_points( } #[node_macro::node(name("Spline"), category("Vector: Modifier"), icon("NodeSpline"), 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. @@ -2620,7 +2621,7 @@ fn apply_point_deltas(vector: &mut Vector, paths: &List, anchors: &[usize] /// Perturbs the positions of anchor points in vector geometry by random amounts and directions. #[node_macro::node(category("Vector: Modifier"), icon("NodeJitterPoints"), path(core_types::vector))] -async fn jitter_points( +fn jitter_points( _: impl Ctx, /// The vector geometry with points to be jittered. #[implementations(Graphic, Vector)] @@ -2672,7 +2673,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"), icon("NodeOffsetPoints"), path(core_types::vector))] -async fn offset_points( +fn offset_points( _: impl Ctx, /// The vector geometry with points to be offset. #[implementations(Graphic, Vector)] @@ -2713,7 +2714,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"), icon("NodeMorph"), 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, @@ -3711,12 +3712,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"), icon("NodeCountPoints"), path(graphene_core::vector))] -async fn count_points(_: impl Ctx, content: Item) -> Item { +fn count_points(_: impl Ctx, content: Item) -> Item { let paths = paths_from_item_attributes(&content).unwrap_or_else(|| no_paths()); let count = content.element().anchor_indices(paths).len() as f64; @@ -3726,7 +3727,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"), icon("NodeIndexPoints"), 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, @@ -3752,7 +3753,7 @@ async fn index_points( } #[node_macro::node(category("Vector: Measure"), icon("NodePathLength"), path(core_types::vector))] -async fn path_length(_: impl Ctx, source: Item) -> Item { +fn path_length(_: impl Ctx, source: Item) -> Item { let paths = paths_from_item_attributes(&source).unwrap_or_else(|| no_paths()); let transform: DAffine2 = source.attribute_cloned_or_default(ATTR_TRANSFORM); let length = source @@ -3768,29 +3769,29 @@ async fn path_length(_: impl Ctx, source: Item) -> Item { } #[node_macro::node(category("Vector: Measure"), icon("NodeArea"), 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 paths = paths_from_item_attributes(&vector).unwrap_or_else(|| no_paths()); let area = vector.element().stroke_bezpath_iter(paths).map(|subpath| subpath.area() * area_scale).sum::(); - Item::new_from_element(area) + Ok(Item::new_from_element(area)) } #[node_macro::node(category("Vector: Measure"), icon("NodeCentroid"), 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 paths = paths_from_item_attributes(&vector).unwrap_or_else(|| no_paths()); let position = element_centroid(vector.element(), paths, 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 anchor positions when the weights all vanish. @@ -3825,22 +3826,19 @@ fn element_centroid(element: &Vector, paths: &List, transform: DAffine2, c #[cfg(test)] mod test { use super::*; - use core_types::Node; + use core_types::{Node, Poll}; use kurbo::{CubicBez, ParamCurveNearest, 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()) } } @@ -3940,12 +3938,12 @@ mod test { assert_eq!(vector.position(5), Some(DVec2::new(53., 0.))); } - #[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)).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 paths = item_paths(&offset); let result = offset.element(); @@ -3960,9 +3958,9 @@ mod test { assert!(subpaths > 0); } - #[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; + #[test] + fn delaunay_disconnected_cells_make_one_subpath_per_triangle() { + let result = super::triangulate((), vector_item_from_points(&SQUARE_WITH_CENTER), item(false)); let paths = item_paths(&result); let vector = result.element(); // The square plus its center tessellates into four triangles, each its own closed subpath. @@ -3972,8 +3970,8 @@ mod test { assert!(!vector.use_face_fill(paths)); } - #[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. } @@ -3993,8 +3991,8 @@ mod test { assert_eq!(subpath_winding_signs(&ellipse, &ellipse_paths)[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(), item_paths(&delaunay)) { assert_eq!(sign, expected, "Delaunay subpath winding should match the Rectangle/Ellipse generators"); } @@ -4003,9 +4001,9 @@ 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; + #[test] + fn delaunay_shared_mesh_welds_points_and_shares_edges() { + let result = super::triangulate((), vector_item_from_points(&SQUARE_WITH_CENTER), item(true)); let paths = item_paths(&result); let vector = result.element(); // The connected mesh reuses the five input points and shares edges, so it fills face by face. @@ -4015,9 +4013,9 @@ mod test { assert_eq!(vector.segment_ids(paths).len(), 8); } - #[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; + #[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 paths = item_paths(&result); let vector = result.element(); let cells = vector.stroke_manipulator_groups(paths).filter(|(_, closed)| *closed).count(); @@ -4032,28 +4030,28 @@ 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; + #[test] + fn voronoi_shared_mesh_uses_face_fill() { + let result = super::voronoi_cells((), vector_item_from_points(&SQUARE_WITH_CENTER), item(true)); let paths = item_paths(&result); let vector = result.element(); assert!(vector.use_face_fill(paths)); assert!(!vector.segment_ids(paths).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)).await; + let result = super::voronoi_cells((), vector_item_from_points(&points), item(false)); let paths = item_paths(&result); let vector = result.element(); assert_eq!(vector.anchor_indices(paths).len(), 2); assert_eq!(vector.segment_ids(paths).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.), @@ -4063,7 +4061,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 positions = result.element().anchor_positions(item_paths(&result)); // Relaxation preserves the point count but repositions the interior anchors within the hull. @@ -4081,9 +4079,9 @@ mod test { } } - #[tokio::test] - async fn bounding_box() { - let bounding_box = super::bounding_box((), Vector::item_from_bezpath(&Rect::new(-1., -1., 1., 1.).to_path(DEFAULT_ACCURACY))).await; + #[test] + fn bounding_box() { + let bounding_box = super::bounding_box((), Vector::item_from_bezpath(&Rect::new(-1., -1., 1., 1.).to_path(DEFAULT_ACCURACY))); let paths = item_paths(&bounding_box); let bounding_box = bounding_box.element(); assert_eq!(bounding_box.stroke_manipulator_groups(paths).count(), 1); @@ -4102,7 +4100,7 @@ mod test { let square = Vector::item_from_bezpath(&Rect::new(-1., -1., 1., 1.).to_path(DEFAULT_ACCURACY)); let mut square = List::new_from_item(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 paths = row_paths(&bounding_box, 0); let bounding_box = bounding_box.element(0).unwrap(); assert_eq!(bounding_box.stroke_manipulator_groups(paths).count(), 1); @@ -4120,8 +4118,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); @@ -4140,9 +4138,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_paths = row_paths(&combined, 0); let combined_copy_to_points = combined.element(0).unwrap(); @@ -4158,8 +4155,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(), @@ -4170,16 +4167,15 @@ mod test { Item::new_from_element(0.), Item::new_from_element(0.), Item::new_from_element(false), - ) - .await; + ); let positions = sample_polyline.element().anchor_positions(item_paths(&sample_polyline)); assert_eq!(positions.len(), 4); for (pos, expected) in positions.iter().zip([DVec2::X * 0., DVec2::X * 30., DVec2::X * 60., DVec2::X * 90.]) { 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(), @@ -4190,43 +4186,41 @@ mod test { Item::new_from_element(45.), Item::new_from_element(10.), Item::new_from_element(true), - ) - .await; + ); let positions = sample_polyline.element().anchor_positions(item_paths(&sample_polyline)); assert_eq!(positions.len(), 4); for (pos, expected) in positions.iter().zip([DVec2::X * 45., DVec2::X * 60., DVec2::X * 75., DVec2::X * 90.]) { 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.))), Item::new_from_element(10. * std::f64::consts::SQRT_2), Item::new_from_element(0), - ) - .await; + ); let positions = poisson_points.element().anchor_positions(item_paths(&poisson_points)); assert!((20..=40).contains(&positions.len()), "actual len {}", positions.len()); for point in positions { 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); - 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; + #[test] + fn spline() { + let spline = super::spline(Footprint::default(), vector_item_from_bezpath(Rect::new(0., 0., 100., 100.).to_path(DEFAULT_ACCURACY))); let paths = item_paths(&spline); let spline = spline.element(); assert_eq!(spline.stroke_bezpath_iter(paths).count(), 1); @@ -4239,18 +4233,18 @@ mod test { open.line_to((10., 10.)); open.line_to((20., 0.)); open.line_to((30., 10.)); - let spline = super::spline(Footprint::default(), vector_item_from_bezpath(open)).await; + let spline = super::spline(Footprint::default(), vector_item_from_bezpath(open)); assert_eq!(smooth_anchors_by_contour(&spline), [[false, true, true, false]]); } // A curved source's handles are replaced by the spline's own rather than kept alongside them - #[tokio::test] - async fn spline_replaces_the_handles_of_a_curved_source() { + #[test] + fn spline_replaces_the_handles_of_a_curved_source() { let ellipse = vector_item_from_bezpath(ellipse_bezpath(DVec2::splat(-50.), DVec2::splat(50.))); let anchors = ellipse.element().anchor_positions(item_paths(&ellipse)); assert_eq!(anchors.len(), 4); - let spline = super::spline(Footprint::default(), ellipse).await; + let spline = super::spline(Footprint::default(), ellipse); let paths = item_paths(&spline); let spline = spline.element(); @@ -4260,12 +4254,12 @@ mod test { } // Only the source's anchors become corners, joined by lines in their stored order - #[tokio::test] - async fn points_to_polyline_joins_the_anchors_with_lines() { + #[test] + fn points_to_polyline_joins_the_anchors_with_lines() { let ellipse = vector_item_from_bezpath(ellipse_bezpath(DVec2::splat(-50.), DVec2::splat(50.))); let anchors = ellipse.element().anchor_positions(item_paths(&ellipse)); - let closed = super::points_to_polyline(Footprint::default(), ellipse.clone(), item(true)).await; + let closed = super::points_to_polyline(Footprint::default(), ellipse.clone(), item(true)); let paths = item_paths(&closed); let segments: Vec = closed.element().segment_iter(paths).map(|(_, segment, ..)| segment).collect(); assert_eq!(closed.element().point_count(), anchors.len(), "the source's handles should be dropped"); @@ -4274,11 +4268,11 @@ mod test { assert!(segments.iter().all(|segment| matches!(segment, PathSeg::Line(_)))); assert!(closed.element().stroke_bezpath_iter(paths).all(|bezpath| bezpath.elements().last() == Some(&PathEl::ClosePath))); - let open = super::points_to_polyline(Footprint::default(), ellipse, item(false)).await; + let open = super::points_to_polyline(Footprint::default(), ellipse, item(false)); assert_eq!(open.element().segment_iter(item_paths(&open)).count(), 3); // Two points have nowhere to close back to, so they draw one segment either way - let pair = super::points_to_polyline(Footprint::default(), vector_item_from_points(&[DVec2::ZERO, DVec2::X]), item(true)).await; + let pair = super::points_to_polyline(Footprint::default(), vector_item_from_points(&[DVec2::ZERO, DVec2::X]), item(true)); assert_eq!(pair.element().segment_iter(item_paths(&pair)).count(), 1); } @@ -4294,8 +4288,8 @@ mod test { } // Existing handles are kept, so only the anchors without them are tangented and smooth - #[tokio::test] - async fn auto_tangents_marks_the_tangented_anchors_smooth() { + #[test] + fn auto_tangents_marks_the_tangented_anchors_smooth() { let mut bezpath = BezPath::new(); bezpath.move_to((0., 0.)); bezpath.line_to((10., 10.)); @@ -4311,7 +4305,7 @@ mod test { bezpath.line_to((200., 50.)); bezpath.close_path(); - let output = super::auto_tangents((), vector_item_from_bezpath(bezpath), item(0.5), item(true)).await; + let output = super::auto_tangents((), vector_item_from_bezpath(bezpath), item(0.5), item(true)); assert_eq!( smooth_anchors_by_contour(&output), @@ -4320,8 +4314,8 @@ mod test { } // A loop of one segment has no neighbor to share a tangent with, so it passes through unchanged - #[tokio::test] - async fn auto_tangents_keeps_a_single_segment_loop() { + #[test] + fn auto_tangents_keeps_a_single_segment_loop() { let teardrop = CubicBez::new((0., 0.), (50., -50.), (100., 50.), (0., 0.)); let mut bezpath = BezPath::new(); bezpath.move_to(teardrop.p0); @@ -4332,7 +4326,7 @@ mod test { bezpath.line_to((225., 40.)); bezpath.close_path(); - let output = super::auto_tangents((), vector_item_from_bezpath(bezpath), item(0.5), item(true)).await; + let output = super::auto_tangents((), vector_item_from_bezpath(bezpath), item(0.5), item(true)); let paths = item_paths(&output); let segments: Vec = output.element().segment_iter(paths).map(|(_, segment, ..)| segment).collect(); @@ -4340,8 +4334,8 @@ mod test { assert!(segments.contains(&PathSeg::Cubic(teardrop)), "the loop should keep its curve, not among {segments:?}"); assert_eq!(smooth_anchors_by_contour(&output), [vec![false], vec![true, true, true]]); } - #[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()); @@ -4354,8 +4348,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_positions = morphed.element(0).unwrap().anchor_positions(row_paths(&morphed, 0)); // Geometry stays in local space (original rectangle coordinates) @@ -4364,8 +4357,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 = || Vector::item_from_bezpath(&Rect::new(0., 0., 100., 100.).to_path(DEFAULT_ACCURACY)); let solid_fill = |color: Color| Appearance::new_single(Coverage::new_fill(), Graphic::from(List::new_from_element(color))); @@ -4384,8 +4377,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"); @@ -4399,8 +4391,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 = || Vector::item_from_bezpath(&Rect::new(0., 0., 100., 100.).to_path(DEFAULT_ACCURACY)); let paint_color = |appearance: &Appearance, cover| { @@ -4436,8 +4428,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"); @@ -4468,8 +4459,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 paths = item_paths(&beveled); @@ -4491,8 +4482,8 @@ mod test { contains_segment(beveled, paths, 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(); @@ -4516,8 +4507,8 @@ mod test { contains_segment(beveled, paths, 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(); @@ -4544,8 +4535,8 @@ mod test { contains_segment(beveled, paths, 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.)); @@ -4569,8 +4560,8 @@ mod test { contains_segment(beveled, paths, 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.))); @@ -4586,8 +4577,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); @@ -4601,8 +4592,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") @@ -4626,8 +4616,8 @@ mod test { assert!(visited[0].0.is_some(), "its geometry should be measurable for the automatic gradient bounds"); } - async fn round_corners_of(bezpath: BezPath, transform: DAffine2) -> Vec { - let rounded = super::round_corners((), create_vector_item(bezpath, transform), item(10.), item(0.5), item(100.), item(5.)).await; + fn round_corners_of(bezpath: BezPath, transform: DAffine2) -> Vec { + let rounded = super::round_corners((), create_vector_item(bezpath, transform), item(10.), item(0.5), item(100.), item(5.)); rounded.element().stroke_bezpath_iter(item_paths(&rounded)).collect() } @@ -4638,12 +4628,12 @@ mod test { .fold(f64::INFINITY, f64::min) } - #[tokio::test] - async fn round_corners_leaves_a_circle_unchanged() { + #[test] + fn round_corners_leaves_a_circle_unchanged() { let circle = ellipse_bezpath(DVec2::ZERO, DVec2::splat(200.)); let transform = DAffine2::from_scale_angle_translation(DVec2::splat(1.5), 0.4, DVec2::new(30., -20.)); - let rounded = round_corners_of(circle.clone(), transform).await; + let rounded = round_corners_of(circle.clone(), transform); assert_eq!(rounded.len(), 1); assert_eq!(rounded[0].segments().count(), circle.segments().count(), "a circle has no corners to round"); for (rounded, original) in rounded[0].segments().zip(circle.segments()) { @@ -4651,9 +4641,9 @@ mod test { } } - #[tokio::test] - async fn round_corners_rounds_a_rectangles_corners() { - let rounded = round_corners_of(Rect::new(0., 0., 100., 100.).to_path(DEFAULT_ACCURACY), DAffine2::IDENTITY).await; + #[test] + fn round_corners_rounds_a_rectangles_corners() { + let rounded = round_corners_of(Rect::new(0., 0., 100., 100.).to_path(DEFAULT_ACCURACY), DAffine2::IDENTITY); let segments: Vec = rounded[0].segments().collect(); assert_eq!(segments.len(), 8, "each of the four corners becomes a curve between two edges"); @@ -4668,8 +4658,8 @@ mod test { } } - #[tokio::test] - async fn round_corners_keeps_curves_beside_a_corner() { + #[test] + fn round_corners_keeps_curves_beside_a_corner() { // A half-disc, whose two corners join its arc to its flat side, and whose arc is smooth where its two curves meet let handle = 50. * 0.552_284_749_8; let mut half_disc = BezPath::new(); @@ -4679,7 +4669,7 @@ mod test { half_disc.line_to((-50., 0.)); half_disc.close_path(); - let rounded = round_corners_of(half_disc.clone(), DAffine2::IDENTITY).await; + let rounded = round_corners_of(half_disc.clone(), DAffine2::IDENTITY); assert_eq!(rounded[0].segments().count(), 5, "only the two corners are rounded"); // The arc away from the corners, up to and past the smooth join at its top @@ -4691,14 +4681,14 @@ mod test { assert!(distance_to_path(DVec2::new(-50., 0.), &rounded[0]) > 1., "the corner should be rounded off"); } - #[tokio::test] - async fn round_corners_leaves_short_subpaths_in_place() { + #[test] + fn round_corners_leaves_short_subpaths_in_place() { let mut line = BezPath::new(); line.move_to((0., 0.)); line.line_to((40., 10.)); let transform = DAffine2::from_scale_angle_translation(DVec2::splat(2.), 0.3, DVec2::new(100., 50.)); - let rounded = round_corners_of(line.clone(), transform).await; + let rounded = round_corners_of(line.clone(), transform); assert_eq!(rounded.len(), 1); assert!( pathseg_abs_diff_eq(rounded[0].segments().next().unwrap(), line.segments().next().unwrap(), 1e-9), diff --git a/node-graph/preprocessor/src/lib.rs b/node-graph/preprocessor/src/lib.rs index 37b2d716765..7ff3adbfda5 100644 --- a/node-graph/preprocessor/src/lib.rs +++ b/node-graph/preprocessor/src/lib.rs @@ -13,10 +13,17 @@ use graphene_std::*; use std::collections::{HashMap, HashSet}; use std::hash::{DefaultHasher, Hash, Hasher}; +#[derive(Debug, Clone)] +struct Substitution { + node: DocumentNode, + wrapped_node_id: NodeId, +} + #[derive(Debug, Default, Clone)] pub struct Preprocessor { - substitutions: HashMap, + substitutions: HashMap, inject_scopes: HashMap, + context_dependencies: HashMap, } impl Preprocessor { @@ -81,10 +88,6 @@ impl Preprocessor { let curve_node = DocumentNode { inputs: vec![NodeInput::value(TaggedValue::None, false), NodeInput::value(TaggedValue::AnimationCurve(curve.clone()), false)], implementation: DocumentNodeImplementation::ProtoNode(graphene_core::animation::evaluate_curve::IDENTIFIER), - context_features: ContextDependencies { - extract: ContextFeatures::ANIMATION_TIME, - inject: ContextFeatures::empty(), - }, ..Default::default() }; new_nodes.push((id, curve_node)); @@ -109,7 +112,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() { @@ -118,6 +123,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 => (), @@ -127,6 +141,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; @@ -138,9 +153,12 @@ impl Preprocessor { fields, memoize, inject_scope, + asynchronous, output_fields, .. } = 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() }); @@ -269,7 +287,7 @@ impl Preprocessor { id.as_str() ); - if generated_nodes == 0 && !memoize && !inject_scope && output_fields.is_none() { + if generated_nodes == 0 && !memoize && !inject_scope && output_fields.is_none() && !asynchronous { continue; } @@ -279,17 +297,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() @@ -297,7 +315,7 @@ impl Preprocessor { nodes.insert(memoize_node_id, memoize_node); memoize_node_id } else { - NodeId(input_count as u64) + wrapped_node_id }; // A multi-output node exports each field through its generated extractor node, after the hidden primary output if it has one @@ -336,7 +354,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 @@ -348,14 +366,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, + } } } @@ -370,7 +391,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 => {} @@ -383,6 +404,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 Inspector widgets @@ -503,11 +525,11 @@ mod destructure_tests { fn assert_execution_result(network: NodeNetwork, expected: TaggedValue) { let proto_network = Compiler {}.compile_single(network).expect("Compilation should succeed"); - let executor = futures::executor::block_on(DynamicExecutor::new(proto_network)).expect("The executor should type check and build"); + let executor = DynamicExecutor::new(proto_network).expect("The executor should type check and build"); let context: core_types::Context = None; - let result = futures::executor::block_on(executor.tree().eval_tagged_value(executor.output(), context)).expect("Execution should succeed"); - assert_eq!(result, expected); + let result = executor.tree().eval_tagged_value(executor.output(), &context).expect("Execution should succeed"); + assert_eq!(result, core_types::Poll::Final(expected)); } #[test] @@ -727,4 +749,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"); + } }