diff --git a/Cargo.lock b/Cargo.lock index 12e730582b..cd6e5d9689 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2038,6 +2038,7 @@ dependencies = [ "core-types", "dyn-any", "glam", + "graphene-hash", "graphene-resource", "log", "raster-types", @@ -4879,15 +4880,11 @@ dependencies = [ "core-types", "dyn-any", "glam", - "graphene-core", "graphic-types", - "kurbo", "log", "node-macro", "raster-types", "serde", - "tokio", - "vector-nodes", "vector-types", ] diff --git a/desktop/src/app.rs b/desktop/src/app.rs index cc20d3abcd..887e19fb88 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -97,6 +97,11 @@ impl App { }); let desktop_wrapper = DesktopWrapper::new(rand::rng().random(), Arc::new(resource_storage), dirs::app_autosave_documents_dir(), wgpu_context.clone(), wake); + let completion_render_sender = start_render_sender.clone(); + DesktopWrapper::set_completion_notifier(move || { + let _ = completion_render_sender.try_send(()); + }); + Self { render_state: None, wgpu_context, diff --git a/desktop/src/render/state.rs b/desktop/src/render/state.rs index 2106446204..b3ced6709e 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.into()); diff --git a/desktop/wrapper/src/lib.rs b/desktop/wrapper/src/lib.rs index 5f2adff705..ff89db7636 100644 --- a/desktop/wrapper/src/lib.rs +++ b/desktop/wrapper/src/lib.rs @@ -52,6 +52,10 @@ impl DesktopWrapper { executor.execute() } + pub fn set_completion_notifier(notifier: impl Fn() + Send + Sync + 'static) { + graphite_editor::node_graph_executor::set_completion_notifier(Arc::new(notifier)); + } + pub async fn execute_node_graph() -> NodeGraphExecutionResult { let result = graphite_editor::node_graph_executor::run_node_graph().await; match result { diff --git a/document/graph-storage/src/tests/round_trip.rs b/document/graph-storage/src/tests/round_trip.rs index 2fa8af0684..95c605f6a8 100644 --- a/document/graph-storage/src/tests/round_trip.rs +++ b/document/graph-storage/src/tests/round_trip.rs @@ -246,10 +246,10 @@ 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 context_features = ContextDependencies::new( + core_types::context::ContextFeatures::FOOTPRINT | core_types::context::ContextFeatures::REAL_TIME, + core_types::context::ContextFeatures::empty(), + ); let network = NodeNetwork { exports: vec![NodeInput::node(NodeId(1), 0)], diff --git a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs index c66d3f9810..1912f5df42 100644 --- a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs +++ b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs @@ -14,7 +14,7 @@ use graphene_std::memo::IORecord; use graphene_std::raster_types::{CPU, GPU, Raster}; use graphene_std::vector::Vector; use graphene_std::vector::style::{FillChoice, FillChoiceUI, GradientSpreadMethod, GradientType}; -use graphene_std::{Artboard, Color, Context, Graphic}; +use graphene_std::{Artboard, Color, CtxSnapshot, Graphic}; use std::any::Any; use std::sync::Arc; @@ -167,7 +167,7 @@ macro_rules! generate_layout_downcast { ($introspected_data:expr, $data:expr, [ $($ty:ty),* $(,)? ]) => { if false { None } $( - else if let Some(io) = $introspected_data.downcast_ref::>() { + else if let Some(io) = $introspected_data.downcast_ref::>() { Some(io.output.layout_with_breadcrumb($data)) } )* @@ -178,7 +178,7 @@ macro_rules! generate_layout_downcast { fn generate_layout(introspected_data: &Arc, data: &mut LayoutData) -> Option> { // `List` is interpreted as a path (e.g. the value produced by `path_of_subgraph`), shown as a // `List` where each item's NodeId resolves against the prefix made up of the items above it. - if let Some(io) = introspected_data.downcast_ref::>>() { + if let Some(io) = introspected_data.downcast_ref::>>() { return Some(table_node_id_path_layout_with_breadcrumb(&io.output, data)); } generate_layout_downcast!(introspected_data, data, [ diff --git a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs index 7372713894..0ec3a05294 100644 --- a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs +++ b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs @@ -919,7 +919,12 @@ fn document_node_definitions() -> HashMap HashMap) -> HashMap // fallback when deriving `call_argument` so it reflects the impls actually registered, which will usually be `Context`. let extended_node_registry = &*interpreted_executor::node_registry::NODE_REGISTRY; let node_registry = NODE_REGISTRY.lock().unwrap(); - let empty_implementations: Vec<(NodeConstructor, NodeIOTypes)> = Vec::new(); + let empty_implementations: Vec = Vec::new(); let context_type = concrete!(Context); for (id, metadata) in NODE_METADATA.lock().unwrap().iter() { let identifier = DefinitionIdentifier::ProtoNode(id.clone()); @@ -48,12 +48,12 @@ pub(super) fn post_process_nodes(custom: Vec) -> HashMap let implementations = node_registry.get(id).unwrap_or(&empty_implementations); - let first_node_io = implementations.first().map(|(_, node_io)| node_io).unwrap_or(const { &NodeIOTypes::empty() }); + let first_node_io = implementations.first().map(|entry| &entry.io).unwrap_or(const { &NodeIOTypes::empty() }); let call_arguments: Vec<&Type> = if !implementations.is_empty() { - implementations.iter().map(|(_, io)| &io.call_argument).collect() + implementations.iter().map(|entry| &entry.io.call_argument).collect() } else if let Some(impls) = extended_node_registry.get(id) { - impls.keys().map(|io| &io.call_argument).collect() + impls.iter().map(|entry| &entry.io.call_argument).collect() } else { Vec::new() }; diff --git a/editor/src/messages/portfolio/document/node_graph/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs index 36eb417ca3..9b400255f4 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -323,6 +323,7 @@ pub(crate) fn property_from_type( Type::Generic(_) => vec![TextLabel::new("Generic Type (Not Supported)").widget_instance()].into(), Type::Fn(_, out) => return property_from_type(node_id, index, out, number_options, unit, display_decimal_places, step, context), Type::Future(out) => return property_from_type(node_id, index, out, number_options, unit, display_decimal_places, step, context), + Type::Ref(inner) => return property_from_type(node_id, index, inner, number_options, unit, display_decimal_places, step, context), }; extra_widgets.push(widgets); @@ -2359,7 +2360,7 @@ pub(crate) fn generate_node_properties(node_id: NodeId, context: &mut NodeProper return Vec::new(); }; - let mut input_types = implementations.keys().filter_map(|item| item.inputs.get(input_index)).collect::>(); + let mut input_types = implementations.iter().filter_map(|entry| entry.io.inputs.get(input_index)).collect::>(); input_types.sort_by_key(|ty| ty.type_name()); let input_type = input_types.first().cloned(); diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface/resolved_types.rs b/editor/src/messages/portfolio/document/utility_types/network_interface/resolved_types.rs index a821ebab79..a618475665 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface/resolved_types.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface/resolved_types.rs @@ -253,8 +253,9 @@ impl NodeNetworkInterface { }; let number_of_inputs = self.number_of_inputs(node_id, network_path); implementations - .keys() - .filter_map(|node_io| { + .iter() + .filter_map(|entry| { + let node_io = &entry.io; // Check if this NodeIOTypes implementation is valid for the other inputs let valid_implementation = (0..number_of_inputs).filter(|iterator_index| iterator_index != input_index).all(|iterator_index| { let input_type = self.input_type_not_invalid(&InputConnector::node(*node_id, iterator_index), network_path); @@ -293,8 +294,9 @@ impl NodeNetworkInterface { let valid_output_types = self.valid_output_types(&OutputConnector::node(*node_id, 0), network_path); implementations - .keys() - .filter_map(|node_io| { + .iter() + .filter_map(|entry| { + let node_io = &entry.io; if !valid_output_types.iter().any(|output_type| output_type.nested_type() == node_io.return_value.nested_type()) { return None; } @@ -323,7 +325,7 @@ impl NodeNetworkInterface { log::error!("Protonode {render_node:?} not found in registry"); return Vec::new(); }; - implementations.keys().map(|types| types.inputs[1].clone()).collect() + implementations.iter().map(|entry| entry.io.inputs[1].clone()).collect() } } } diff --git a/editor/src/messages/portfolio/document_migration.rs b/editor/src/messages/portfolio/document_migration.rs index ec9483d4a2..4bad328df5 100644 --- a/editor/src/messages/portfolio/document_migration.rs +++ b/editor/src/messages/portfolio/document_migration.rs @@ -2406,7 +2406,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], && 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.document_node.context_features; + let context_features = node_definition.node_template.document_node.context_features.clone(); document.network_interface.set_context_features(node_id, network_path, context_features); } diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index 18e7b77a20..2cbc23297a 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -15,7 +15,7 @@ use graphene_std::raster::{CPU, Raster}; use graphene_std::renderer::{RenderMetadata, graphic_list_bounding_box}; use graphene_std::transform::Footprint; use graphene_std::vector::{Vector, graphic_types}; -use graphene_std::{ATTR_TRANSFORM, Context, Graphic, NodeInputDecleration}; +use graphene_std::{ATTR_TRANSFORM, CtxSnapshot, Graphic, NodeInputDecleration}; use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypesDelta; use std::any::Any; use std::sync::Arc; @@ -26,7 +26,7 @@ pub use runtime_io::NodeRuntimeIO; mod runtime; pub use runtime::*; -#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct ExecutionRequest { execution_id: u64, render_config: RenderConfig, @@ -59,6 +59,9 @@ pub struct NodeGraphExecutor { runtime_io: NodeRuntimeIO, current_execution_id: u64, futures: VecDeque<(u64, ExecutionContext)>, + /// The most recently consumed plain render execution, kept so a runtime-replayed response with the same id + /// (sent after an async source completion) finds its context again. + last_execution_context: 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, @@ -108,6 +111,7 @@ impl NodeGraphExecutor { let node_executor = Self { futures: Default::default(), runtime_io: NodeRuntimeIO::with_channels(request_sender, response_receiver), + last_execution_context: None, node_graph_hash: 0, current_execution_id: 0, previous_node_to_inspect: Vec::new(), @@ -375,10 +379,22 @@ 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("front was just matched"); + self.last_execution_context = Some((execution_id, execution_context.clone())); + execution_context + } else { + // A runtime-replayed response re-uses an already consumed id; only plain renders may re-apply. + match &self.last_execution_context { + Some((last_execution_id, execution_context)) if *last_execution_id == execution_id => { + if execution_context.export_config.is_some() || execution_context.measure_fill.is_some() { + continue; + } + execution_context.clone() + } + _ => panic!("InvalidGenerationId"), + } }; - 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. @@ -892,7 +908,7 @@ fn introspected_output(data: &Arc>() { return Some(io.output.clone()); } - if let Some(io) = data.downcast_ref::>() { + if let Some(io) = data.downcast_ref::>() { return Some(io.output.clone()); } None @@ -911,7 +927,7 @@ mod test { use crate::test_utils::test_prelude::{self, NodeGraphLayer}; use graph_craft::ProtoNodeIdentifier; use graph_craft::document::NodeNetwork; - use graphene_std::Context; + use graphene_std::CtxSnapshot; use graphene_std::NodeInputDecleration; use graphene_std::memo::IORecord; use test_prelude::LayerNodeIdentifier; @@ -979,7 +995,7 @@ mod test { Some(x.output.clone()) } else if let Some(x) = dynamic.downcast_ref::>() { Some(x.output.clone()) - } else if let Some(x) = dynamic.downcast_ref::>() { + } else if let Some(x) = dynamic.downcast_ref::>() { Some(x.output.clone()) } else { warn!("cannot downcast type for introspection"); diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index 91e9ea8049..fbfa1f09b5 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -3,25 +3,26 @@ use crate::messages::frontend::utility_types::{ExportBounds, FileType}; use glam::{DAffine2, DVec2, UVec2}; use graph_craft::application_io::resource::ResourceRegistry; use graph_craft::application_io::{PlatformApplicationIo, PlatformEditorApi}; -use graph_craft::concrete; 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::bounds::RenderBoundingBox; +use graphene_std::core_types::gpoll::GPoll; use graphene_std::list::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::{DynGraphRuntime, DynNotifier, DynSpawner, GraphRuntime, RuntimeHandle, SourceFuture, Spawner, poll_once}; use graphene_std::transform::RenderQuality; use graphene_std::vector::Vector; use graphene_std::vector::style::RenderMode; -use graphene_std::{Artboard, Context, Graphic}; -use interpreted_executor::dynamic_executor::{DynamicExecutor, IntrospectError, ResolvedDocumentNodeTypesDelta}; +use graphene_std::{Artboard, CtxSnapshot, Graphic}; +use interpreted_executor::dynamic_executor::{DynamicExecutor, ResolvedDocumentNodeTypesDelta}; use interpreted_executor::util::wrap_network_in_scope; use spin::Mutex; use std::sync::Arc; @@ -40,6 +41,9 @@ pub struct NodeRuntime { editor_preferences: EditorPreferences, old_graph: Option, update_thumbnails: bool, + graph_runtime: Arc, + /// The last plain render request, replayed when an async source completion marks the graph dirty. + last_render: Option, editor_api: Arc, resources: ResourceRegistry, @@ -119,20 +123,86 @@ impl NodeGraphUpdateSender for InternalNodeGraphUpdateSender { // TODO: Replace with `core::cell::LazyCell` () or similar pub static NODE_RUNTIME: once_cell::sync::Lazy>> = once_cell::sync::Lazy::new(|| Mutex::new(None)); +#[cfg(not(target_family = "wasm"))] +pub struct TokioSpawner(Option); + +#[cfg(not(target_family = "wasm"))] +impl TokioSpawner { + pub fn new() -> Self { + Self(Some(tokio::runtime::Runtime::new().expect("Failed to start the async source runtime"))) + } +} + +#[cfg(not(target_family = "wasm"))] +impl Default for TokioSpawner { + fn default() -> Self { + Self::new() + } +} + +#[cfg(not(target_family = "wasm"))] +impl Spawner for TokioSpawner { + fn spawn(&self, mut task: SourceFuture) -> bool { + let runtime = self.0.as_ref().expect("runtime lives until drop"); + let _guard = runtime.enter(); + if poll_once(&mut task) { + return true; + } + runtime.spawn(task); + false + } +} + +/// Dropping a tokio runtime blocks on its tasks, which panics inside an async context; the tests drop +/// [`NodeRuntime`] from one, so shut down in the background instead. +#[cfg(not(target_family = "wasm"))] +impl Drop for TokioSpawner { + fn drop(&mut self) { + if let Some(runtime) = self.0.take() { + runtime.shutdown_background(); + } + } +} + +#[cfg(target_family = "wasm")] +pub struct WasmSpawner; + +#[cfg(target_family = "wasm")] +impl Spawner for WasmSpawner { + fn spawn(&self, mut task: SourceFuture) -> bool { + if poll_once(&mut task) { + return true; + } + wasm_bindgen_futures::spawn_local(task); + false + } +} + impl NodeRuntime { pub fn new(receiver: Receiver, sender: Sender) -> Self { + #[cfg(not(target_family = "wasm"))] + let spawner: Box = Box::new(TokioSpawner::new()); + #[cfg(target_family = "wasm")] + let spawner: Box = Box::new(WasmSpawner); + let graph_runtime: Arc = Arc::new(GraphRuntime::new(spawner)); + let mut executor = DynamicExecutor::default(); + executor.set_runtime(Arc::clone(&graph_runtime)); + Self { - executor: DynamicExecutor::default(), + executor, receiver, sender: InternalNodeGraphUpdateSender(sender.clone()), editor_preferences: EditorPreferences::default(), old_graph: None, resources: ResourceRegistry::default(), update_thumbnails: true, + graph_runtime: Arc::clone(&graph_runtime), + last_render: None, editor_api: PlatformEditorApi { editor_preferences: Box::new(EditorPreferences::default()), node_graph_message_sender: Box::new(InternalNodeGraphUpdateSender(sender)), + runtime: RuntimeHandle(graph_runtime), #[cfg(not(test))] application_io: None, @@ -157,6 +227,11 @@ impl NodeRuntime { } } + #[cfg(test)] + pub fn take_dirty(&self) -> bool { + self.executor.take_dirty() + } + pub async fn run(&mut self) -> Option { let mut preferences = None; let mut graph = None; @@ -173,6 +248,9 @@ impl NodeRuntime { } let for_export = execution_request.render_config.for_export; + if !for_export { + self.last_render = Some(execution_request.clone()); + } execution = Some(request); @@ -193,6 +271,10 @@ impl NodeRuntime { eyedropper.render_config.pointer = execution.render_config.pointer; } + if self.executor.take_dirty() && execution.is_none() { + execution = self.last_render.clone().map(GraphRuntimeRequest::ExecutionRequest); + } + let requests = [preferences, graph, eyedropper, execution].into_iter().flatten(); for request in requests { @@ -203,11 +285,12 @@ impl NodeRuntime { application_io: self.editor_api.application_io.clone(), node_graph_message_sender: Box::new(self.sender.clone()), editor_preferences: Box::new(preferences), + runtime: self.editor_api.runtime.clone(), } .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,7 +305,7 @@ impl NodeRuntime { self.resources = resources; self.node_graph_errors.clear(); - let result = self.update_network(network).await; + let result = self.update_network(network); let node_graph_errors = self.node_graph_errors.clone(); self.update_thumbnails = true; @@ -237,7 +320,7 @@ impl NodeRuntime { render_config.export_format = ExportFormat::Svg; } - let result = self.execute_network(render_config).await; + let result = self.execute_network(render_config); let mut responses = VecDeque::new(); // TODO: Only process monitor nodes if the graph has changed, not when only the Footprint changes if !render_config.for_eyedropper { @@ -258,10 +341,10 @@ impl NodeRuntime { .application_io .as_ref() .unwrap() - .gpu_executor() + .gpu_executor_arc() .expect("GPU executor should be available when we receive a texture"); - let raster_cpu = Raster::new_gpu(texture).convert(Footprint::BOUNDLESS, executor).await; + let raster_cpu = Raster::new_gpu(texture).convert(Footprint::BOUNDLESS, wgpu_executor::WgpuExecutorHandle(executor)).await; let (data, width, height) = raster_cpu.to_flat_u8(); @@ -282,10 +365,10 @@ impl NodeRuntime { .application_io .as_ref() .unwrap() - .gpu_executor() + .gpu_executor_arc() .expect("GPU executor should be available when we receive a texture"); - let raster_cpu = Raster::new_gpu(texture).convert(Footprint::BOUNDLESS, executor).await; + let raster_cpu = Raster::new_gpu(texture).convert(Footprint::BOUNDLESS, wgpu_executor::WgpuExecutorHandle(executor)).await; self.sender.send_eyedropper_preview(raster_cpu); continue; @@ -345,7 +428,7 @@ impl NodeRuntime { None } - async fn update_network(&mut self, graph: NodeNetwork) -> Result { + fn update_network(&mut self, graph: NodeNetwork) -> Result { let mut scoped_network = wrap_network_in_scope(graph, self.editor_api.clone()); if let Err(e) = self.preprocessor.preprocess(&mut scoped_network, &|resource_id| self.resources.hash(&resource_id)) { @@ -368,20 +451,24 @@ impl NodeRuntime { .collect::>(); assert_ne!(proto_network.nodes.len(), 0, "No proto nodes exist?"); - self.executor.update(proto_network).await.map_err(|(types, e)| { + self.executor.update(proto_network).map_err(|(types, e)| { self.node_graph_errors.clone_from(&e); (types, format!("{e:?}")) }) } - async fn execute_network(&mut self, render_config: RenderConfig) -> Result { + fn execute_network(&mut self, render_config: RenderConfig) -> Result { use graph_craft::graphene_compiler::Executor; - match self.executor.input_type() { - Some(t) if t == concrete!(RenderConfig) => (&self.executor).execute(render_config).await.map_err(|e| e.to_string()), - Some(t) if t == concrete!(()) => (&self.executor).execute(()).await.map_err(|e| e.to_string()), - Some(t) => Err(format!("Invalid input type {t:?}")), - _ => Err(format!("No input type:\n{:?}", self.node_graph_errors)), + match (&self.executor).execute(render_config).map_err(|e| e.to_string())? { + GPoll::Final(value) | GPoll::Partial(value) => Ok(value), + GPoll::Fallback(boxed) => { + let (value, error) = *boxed; + error!("Node graph evaluation reported an error alongside its fallback output: {error:?}"); + Ok(value) + } + GPoll::Pending => Err("Node graph evaluation is pending".to_string()), + GPoll::Error(error) => Err(format!("Node graph evaluation failed: {error:?}")), } } @@ -415,7 +502,7 @@ impl NodeRuntime { }; // Graphic list: thumbnail (text-aware bounds, since the `BoundingBox` trait can't lay out `Graphic::Text` content) - if let Some(io) = introspected_data.downcast_ref::>>() { + if let Some(io) = introspected_data.downcast_ref::>>() { if update_thumbnails { let bounds = graphene_std::renderer::graphic_list_bounding_box(&io.output, DAffine2::IDENTITY); Self::render_thumbnail(&mut self.thumbnail_renders, parent_network_node_id, &io.output, bounds, responses) @@ -423,19 +510,19 @@ impl NodeRuntime { } // Artboard thumbnail bounds come from the clipping rectangles, not the content union, since the renderer // clips content to those rectangles so anything outside isn't visible - else if let Some(io) = introspected_data.downcast_ref::>>() { + else if let Some(io) = introspected_data.downcast_ref::>>() { if update_thumbnails { let bounds = artboard_clip_bounds(&io.output); Self::render_thumbnail(&mut self.thumbnail_renders, parent_network_node_id, &io.output, bounds, responses) } } // Vector list: vector modifications - else if let Some(io) = introspected_data.downcast_ref::>>() { + else if let Some(io) = introspected_data.downcast_ref::>>() { // Insert the vector modify self.vector_modify.insert(parent_network_node_id, io.output.element(0).cloned().unwrap_or_default()); } // String list: thumbnail (bounds need text layout, which the `BoundingBox` trait can't do for a bare `String`) - else if let Some(io) = introspected_data.downcast_ref::>>() { + else if let Some(io) = introspected_data.downcast_ref::>>() { if update_thumbnails { let bounds = graphene_std::renderer::text_list_bounding_box(&io.output, DAffine2::IDENTITY); Self::render_thumbnail(&mut self.thumbnail_renders, parent_network_node_id, &io.output, bounds, responses) @@ -544,14 +631,6 @@ fn expand_to_thumbnail_aspect(bounds: [DVec2; 2]) -> [DVec2; 2] { [center - half, center + half] } -pub async fn introspect_node(path: &[NodeId]) -> Result, IntrospectError> { - let runtime = NODE_RUNTIME.lock(); - if let Some(ref mut runtime) = runtime.as_ref() { - return runtime.executor.introspect(path); - } - Err(IntrospectError::RuntimeNotReady) -} - pub async fn run_node_graph() -> (bool, Option) { let Some(mut runtime) = NODE_RUNTIME.try_lock() else { return (false, None) }; if let Some(ref mut runtime) = runtime.as_mut() { @@ -571,12 +650,20 @@ pub(crate) fn replace_application_io(application_io: PlatformApplicationIo) { } } +pub fn set_completion_notifier(notifier: Arc) { + let node_runtime = NODE_RUNTIME.lock(); + if let Some(node_runtime) = &*node_runtime { + node_runtime.graph_runtime.set_notifier(notifier); + } +} + 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()), + runtime: self.editor_api.runtime.clone(), } .into(); } diff --git a/editor/src/test_utils.rs b/editor/src/test_utils.rs index 84f67f0418..c0754313d1 100644 --- a/editor/src/test_utils.rs +++ b/editor/src/test_utils.rs @@ -53,6 +53,27 @@ impl EditorTestUtils { } runtime.run().await; + // An async source reports `Pending` on the evaluation that starts it and marks the runtime dirty once + // it completes, so the value only reaches the render on a follow-up evaluation. The superseded response's + // `Pending` error is ignored, but its messages carry the incremental resolved-types delta and must be + // dispatched, or the editor's type map desyncs permanently. + while runtime.take_dirty() { + let mut superseded_messages = VecDeque::new(); + let _ = editor.poll_node_graph_evaluation(&mut superseded_messages); + for message in superseded_messages { + editor.handle_message(message); + } + + let portfolio = &mut editor.dispatcher.message_handlers.portfolio_message_handler; + let (executor, documents) = (&mut portfolio.executor, &mut portfolio.documents); + let document = documents.get_mut(&document_id).unwrap(); + + if let Err(e) = executor.submit_current_node_graph_evaluation(document, document_id, UVec2::ONE, 1., Default::default(), DVec2::ZERO) { + return Err(format!("submit_current_node_graph_evaluation failed\n\n{e}")); + } + runtime.run().await; + } + let mut messages = VecDeque::new(); if let Err(e) = editor.poll_node_graph_evaluation(&mut messages) { return Err(format!("Graph should render\n\n{e}")); diff --git a/node-graph/graph-craft/src/application_io.rs b/node-graph/graph-craft/src/application_io.rs index cd2304da31..4e4ebc5b95 100644 --- a/node-graph/graph-craft/src/application_io.rs +++ b/node-graph/graph-craft/src/application_io.rs @@ -9,7 +9,7 @@ pub use graphene_application_io::ApplicationIo; #[derive(Default)] pub struct PlatformApplicationIo { #[cfg(feature = "wgpu")] - gpu_executor: Option, + gpu_executor: Option>, resources: Option>, } @@ -26,7 +26,7 @@ impl PlatformApplicationIo { Self { #[cfg(feature = "wgpu")] - gpu_executor: executor, + gpu_executor: executor.map(std::sync::Arc::new), resources: None, } } @@ -39,7 +39,7 @@ impl PlatformApplicationIo { set_wgpu_available(wgpu_available); Self { - gpu_executor: executor, + gpu_executor: executor.map(std::sync::Arc::new), resources: None, } } @@ -57,7 +57,12 @@ impl ApplicationIo for PlatformApplicationIo { #[cfg(feature = "wgpu")] fn gpu_executor(&self) -> Option<&Self::Executor> { - self.gpu_executor.as_ref() + self.gpu_executor.as_deref() + } + + #[cfg(feature = "wgpu")] + fn gpu_executor_arc(&self) -> Option> { + self.gpu_executor.clone() } fn load_resource(&self, hash: resource::ResourceHash) -> resource::ResourceFuture<'_> { diff --git a/node-graph/graph-craft/src/document.rs b/node-graph/graph-craft/src/document.rs index bff579d6a5..7b850324c0 100644 --- a/node-graph/graph-craft/src/document.rs +++ b/node-graph/graph-craft/src/document.rs @@ -8,7 +8,6 @@ pub use core_types::uuid::generate_uuid; use core_types::{Context, ContextDependencies, Cow, MemoHash, ProtoNodeIdentifier, Type}; use dyn_any::DynAny; use glam::IVec2; -use log::Metadata; use rustc_hash::FxHashMap; use std::collections::HashMap; use std::collections::hash_map::DefaultHasher; @@ -215,12 +214,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 => concrete!(core_types::list::List), + DocumentNodeMetadata::SourceId => concrete!(u64), } } } @@ -273,7 +274,7 @@ impl NodeInput { NodeInput::Import { import_type, .. } => import_type.clone(), NodeInput::Inline(_) => panic!("ty() called on NodeInput::Inline"), NodeInput::Scope(_) => panic!("ty() called on NodeInput::Scope"), - NodeInput::Reflection(_) => concrete!(Metadata), + NodeInput::Reflection(metadata) => metadata.ty(), } } @@ -879,7 +880,7 @@ impl NodeNetwork { // Replace value inputs with dedicated value nodes if node.implementation != DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("core_types::value::ClonedNode")) { - Self::replace_value_inputs_with_nodes(&mut node.inputs, &mut self.nodes, &path, gen_id, map_ids, id); + Self::replace_value_inputs_with_nodes(&mut node.inputs, &mut self.nodes, &path, gen_id, map_ids, id, Some(&mut node.context_features)); } let DocumentNodeImplementation::Network(mut inner_network) = node.implementation else { @@ -898,6 +899,7 @@ impl NodeNetwork { gen_id, map_ids, id, + None, ); // Connect all network inputs to either the parent network nodes, or newly created value nodes for the parent node. @@ -978,6 +980,12 @@ impl NodeNetwork { } } + fn source_id_for_path(path: &[NodeId]) -> u64 { + let mut hasher = graphene_hash::FxHasher64::new(); + path.hash(&mut hasher); + hasher.finish() + } + #[inline(never)] fn replace_value_inputs_with_nodes( inputs: &mut [NodeInput], @@ -986,6 +994,7 @@ impl NodeNetwork { gen_id: impl Fn() -> NodeId + Copy, map_ids: impl Fn(NodeId, NodeId) -> NodeId + Copy, id: NodeId, + mut context_features: Option<&mut ContextDependencies>, ) { // Replace value exports and imports with value nodes, added inside the nested network for export in inputs { @@ -996,6 +1005,13 @@ impl NodeNetwork { NodeInput::Value { tagged_value, exposed } => (tagged_value, exposed), NodeInput::Reflection(reflect) => match reflect { DocumentNodeMetadata::DocumentNodePath => (TaggedValue::NodeIdPath(path.to_vec()).into(), false), + DocumentNodeMetadata::SourceId => { + let source_id = Self::source_id_for_path(path); + if let Some(context_features) = context_features.as_deref_mut() { + context_features.add_sources(&[source_id]); + } + (TaggedValue::U64(source_id).into(), false) + } }, previous_export => { *export = previous_export; @@ -1208,7 +1224,9 @@ fn migrate_call_argument<'de, D: serde::Deserializer<'de>>(deserializer: D) -> R Old(Option), } + // TODO: Eventually remove this migration document upgrade code Ok(match CallArg::deserialize(deserializer)? { + CallArg::New(Type::Concrete(descriptor)) if descriptor.name.ends_with("OwnedContextImpl>>") => concrete!(Context), CallArg::New(ty) => ty, CallArg::Old(ty) => ty.unwrap_or_default(), }) diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index c3af90cf23..30d30f5507 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -1,13 +1,18 @@ 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; use brush_nodes::brush_stroke::BrushStroke; use core_types::color::SRGBA8; +use core_types::context::Context; +use core_types::gpoll::GPoll; use core_types::list::List; +use core_types::node::Node; +use core_types::registry::{EdgeHandle, edge_type}; use core_types::transform::Footprint; use core_types::uuid::NodeId; -use core_types::{CacheHash, Color, ContextFeatures, MemoHash, Node, Type, TypeDescriptor}; +use core_types::value::value_edge; +use core_types::{CacheHash, Color, ContextModification, MemoHash, Type, TypeDescriptor}; use dyn_any::DynAny; pub use dyn_any::StaticType; pub use glam::{DAffine2, DVec2, IVec2, UVec2}; @@ -19,7 +24,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; @@ -90,7 +94,7 @@ 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)] EditorApi(Arc), /// Only used by the `resource` node, should never be serialized @@ -120,7 +124,7 @@ macro_rules! tagged_value { // ======================= Self::NodeIdPath(path) => path.hash(state), Self::DocumentNode(node) => node.cache_hash(state), - Self::ContextFeatures(features) => features.cache_hash(state), + Self::ContextModification(modification) => modification.cache_hash(state), Self::RenderOutput(x) => x.cache_hash(state), Self::EditorApi(x) => x.cache_hash(state), Self::ResourceHash(x) => x.cache_hash(state), @@ -175,7 +179,7 @@ macro_rules! tagged_value { Box::new(list) } Self::DocumentNode(node) => Box::new(node), - Self::ContextFeatures(features) => Box::new(features), + Self::ContextModification(modification) => Box::new(modification), Self::EditorApi(x) => Box::new(x), Self::ResourceHash(x) => Box::new(x), } @@ -225,7 +229,7 @@ macro_rules! tagged_value { Arc::new(list) } Self::DocumentNode(node) => Arc::new(node), - Self::ContextFeatures(features) => Arc::new(features), + Self::ContextModification(modification) => Arc::new(modification), Self::EditorApi(x) => Arc::new(x), Self::ResourceHash(x) => Arc::new(x), } @@ -253,12 +257,88 @@ macro_rules! tagged_value { Self::RenderOutput(_) => concrete!(RenderOutput), Self::NodeIdPath(_) => concrete!(List), Self::DocumentNode(_) => concrete!(DocumentNode), - Self::ContextFeatures(_) => concrete!(ContextFeatures), - Self::EditorApi(_) => concrete!(&PlatformEditorApi), + Self::ContextModification(_) => concrete!(ContextModification), + Self::EditorApi(_) => concrete!(Arc), Self::ResourceHash(_) => concrete!(ResourceHash), } } + /// Materializes the value as [`Self::to_dynany`] does, wrapped in a `ClonedNode` edge typed by [`Self::ty`]. + pub fn to_edge(self) -> Result { + match self { + // =============== + // MANUAL VARIANTS + // =============== + Self::None => Ok(value_edge(())), + Self::TypeDefault(td) => { + // Same direct-construction path as `to_dynany` for the same reason as in `to_dynany`. + let name = td.name.as_ref(); + macro_rules! check { + ($type_default:ty) => { + if name == std::any::type_name::<$type_default>() { return Ok(value_edge(<$type_default>::default())); } + }; + } + for_each_type_default!(check); + Self::from_type_or_none(&Type::Concrete(td)).to_edge() + } + Self::F64Array(values) => { + let list: List = values.into_iter().map(core_types::list::Item::new_from_element).collect(); + Ok(value_edge(list)) + } + Self::Color(color) => { + let list: List = color.into_iter().map(core_types::list::Item::new_from_element).collect(); + Ok(value_edge(list)) + } + Self::Gradient(stops) => Ok(value_edge(List::::new_from_element(stops))), + Self::BrushStrokes(strokes) => { + let list: List = strokes.into_iter().map(core_types::list::Item::new_from_element).collect(); + Ok(value_edge(list)) + } + // ======================= + // AUTO-GENERATED VARIANTS + // ======================= + $( Self::$identifier(x) => Ok(value_edge(x)), )* + // ======================= + // NON-SERIALIZED VARIANTS + // ======================= + Self::RenderOutput(x) => Ok(value_edge(x)), + Self::NodeIdPath(path) => { + let list: List = path.into_iter().map(core_types::list::Item::new_from_element).collect(); + Ok(value_edge(list)) + } + Self::DocumentNode(node) => Ok(value_edge(node)), + Self::ContextModification(modification) => Ok(value_edge(modification)), + Self::EditorApi(x) => Ok(value_edge(x)), + Self::ResourceHash(x) => Ok(value_edge(x)), + } + } + + /// Evaluates a typed edge and converts the landed value into a tagged value, with the coverage of [`Self::try_from_any`]. + pub fn from_edge(handle: EdgeHandle, ctx: &Context) -> Result, String> { + let ty = handle.ty().clone(); + // =============== + // MANUAL VARIANTS + // =============== + if ty == edge_type::<()>() { + return Ok(handle.downcast::<()>().map_err(|e| format!("{e:?}"))?.eval(ctx).map(|_| TaggedValue::None)); + } + // ======================= + // AUTO-GENERATED VARIANTS + // ======================= + $( + if ty == edge_type::<$ty>() { + return Ok(handle.downcast::<$ty>().map_err(|e| format!("{e:?}"))?.eval(ctx).map(TaggedValue::$identifier)); + } + )* + // ======================= + // NON-SERIALIZED VARIANTS + // ======================= + if ty == edge_type::() { + return Ok(handle.downcast::().map_err(|e| format!("{e:?}"))?.eval(ctx).map(TaggedValue::RenderOutput)); + } + Err(format!("Cannot convert edge of type {ty} to TaggedValue")) + } + /// Attempts to downcast the dynamic type to a tagged value pub fn try_from_any(input: Box + 'a>) -> Result { use dyn_any::downcast; @@ -309,6 +389,7 @@ macro_rules! tagged_value { pub fn from_type(input: &Type) -> Option { match input { Type::Generic(_) => None, + Type::Ref(_) => None, Type::Concrete(concrete_type) => { let name = concrete_type.name.as_ref(); // TODO: Add default implementations for types such as TaggedValue::Subpaths, and use the defaults here and in document_node_types @@ -359,7 +440,7 @@ 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::EditorApi(_) => "PlatformEditorApi".to_string(), Self::ResourceHash(hash) => format!("ResourceHash({hash:?})"), } @@ -569,6 +650,7 @@ impl TaggedValue { match ty { Type::Generic(_) => None, + Type::Ref(_) => None, Type::Concrete(concrete_type) => { let ty = concrete_type.id?; use std::any::TypeId; @@ -684,39 +766,6 @@ impl Display for TaggedValue { } } -pub struct UpcastNode { - value: MemoHash, -} -impl<'input> Node<'input, DAny<'input>> for UpcastNode { - type Output = FutureAny<'input>; - - 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() }) - } -} -impl UpcastNode { - pub fn new(value: MemoHash) -> Self { - 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(self.0.as_ref()) as DAny<'i> }) - } -} - -impl + Sync + Send, U: Sync + Send> UpcastAsRefNode { - pub const fn new(value: T) -> UpcastAsRefNode { - UpcastAsRefNode(value, PhantomData) - } -} - #[derive(Debug, Clone, PartialEq, dyn_any::DynAny, serde::Serialize, serde::Deserialize)] pub struct RenderOutput { pub data: RenderOutputType, diff --git a/node-graph/graph-craft/src/graphene_compiler.rs b/node-graph/graph-craft/src/graphene_compiler.rs index 22dac658a9..fbf5be5a65 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 2fa7eb93a3..52132b5d3f 100644 --- a/node-graph/graph-craft/src/proto.rs +++ b/node-graph/graph-craft/src/proto.rs @@ -294,6 +294,10 @@ impl ProtoNetwork { (inwards_edges, id_map) } + pub fn source_ids(&self) -> Vec { + self.nodes.iter().flat_map(|(_, node)| node.context_features.sources().iter().copied()).collect() + } + /// Inserts context nullification nodes to optimize caching. /// This analysis is performed after topological sorting to ensure proper dependency tracking. pub fn insert_context_nullification_nodes(&mut self) -> Result<(), String> { @@ -308,7 +312,7 @@ impl ProtoNetwork { Ok(()) } - fn insert_context_nullification_node(&mut self, node_id: NodeId, context_deps: ContextFeatures) -> NodeId { + fn insert_context_nullification_node(&mut self, node_id: NodeId, context_deps: ContextModification) -> NodeId { let (_, node) = &self.nodes[node_id.0 as usize]; let mut path = node.original_location.path.clone(); @@ -338,7 +342,7 @@ impl ProtoNetwork { self.nodes.push(( nullification_value_node_id, ProtoNode { - construction_args: ConstructionArgs::Value(MemoHash::new(TaggedValue::ContextFeatures(context_deps))), + construction_args: ConstructionArgs::Value(MemoHash::new(TaggedValue::ContextModification(context_deps))), call_argument: concrete!(Context), identifier: ProtoNodeIdentifier::new("core_types::value::ClonedNode"), original_location: OriginalLocation { @@ -365,39 +369,43 @@ impl ProtoNetwork { nullification_node_id } - fn find_context_dependencies(&mut self, id: NodeId) -> (ContextFeatures, Option) { + 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 (extract, inject, own_deps) = { + let dependencies = &self.nodes[node_index].1.context_features; + let own_deps = ContextModification::from_sources(dependencies.extract, dependencies.sources()); + (dependencies.extract, dependencies.inject, own_deps) + }; let mut inputs = match &self.nodes[node_index].1.construction_args { // We pretend like we have already placed context modification nodes after ourselves because value nodes don't need to be cached - ConstructionArgs::Value(_) => return (context_features.extract, Some(id)), + ConstructionArgs::Value(_) => return (own_deps, Some(id)), ConstructionArgs::Nodes(items) => items.clone(), - ConstructionArgs::Inline(_) => return (context_features.extract, Some(id)), + ConstructionArgs::Inline(_) => return (own_deps, 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 |= &branch.0; branch_dependencies.push(branch); - combined_deps |= branch.0; } - let mut new_deps = combined_deps; + let mut new_deps = combined_deps.clone(); // Remove requirements which this node provides - new_deps &= !context_features.inject; + new_deps &= !inject; // Add requirements we have - new_deps |= context_features.extract; + new_deps |= own_deps; // 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_introduce_new_deps = !combined_deps.contains(&new_deps); // 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.into_iter()) { + 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 { @@ -407,18 +415,18 @@ impl ProtoNetwork { self.nodes[node_index].1.construction_args = ConstructionArgs::Nodes(inputs); // Which dependencies do we supply (and don't need ourselves)? - let net_injections = context_features.inject.difference(context_features.extract); + let net_injections = inject.difference(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 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) @@ -545,6 +553,7 @@ pub enum GraphErrorType { }, NoImplementations, NoConstructor, + ConstructionFailed(String), /// The `inputs` represents a formatted list of input indices corresponding to their types. /// Each element in `error_inputs` represents a valid `NodeIOTypes` implementation. /// The inner Vec stores the inputs which need to be changed and what type each needs to be changed to. @@ -565,6 +574,7 @@ impl Debug for GraphErrorType { GraphErrorType::UnexpectedGenerics { index, inputs } => write!(f, "Generic inputs should not exist but found at {index}: {inputs:?}"), GraphErrorType::NoImplementations => write!(f, "No implementations found"), GraphErrorType::NoConstructor => write!(f, "No construct found for node"), + GraphErrorType::ConstructionFailed(error) => write!(f, "Construction failed: {error}"), GraphErrorType::InvalidImplementations { inputs, error_inputs } => { let format_error = |(index, (found, expected)): &(usize, (Type, Type))| { let index = index + 1; @@ -631,14 +641,14 @@ pub type GraphErrors = Vec; /// The `TypingContext` is used to store the types of the nodes indexed by their stable node id. #[derive(Default, Clone, dyn_any::DynAny)] pub struct TypingContext { - lookup: Cow<'static, HashMap>>, + lookup: Cow<'static, HashMap>>, inferred: HashMap, constructor: HashMap, } impl TypingContext { /// Creates a new `TypingContext` with the given lookup table. - pub fn new(lookup: &'static HashMap>) -> Self { + pub fn new(lookup: &'static HashMap>) -> Self { Self { lookup: Cow::Borrowed(lookup), ..Default::default() @@ -682,7 +692,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); } @@ -702,6 +712,7 @@ impl TypingContext { // Get the node input type from the proto node declaration let call_argument = &node.call_argument; let impls = self.lookup.get(&node.identifier).ok_or_else(|| vec![GraphError::new(node, GraphErrorType::NoImplementations)])?; + let candidates: Vec<(NodeIOTypes, NodeConstructor)> = impls.iter().map(|entry| (entry.io.clone(), entry.constructor)).collect(); if let Some(index) = inputs.iter().position(|p| { matches!(p, @@ -716,8 +727,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. @@ -740,25 +749,24 @@ impl TypingContext { } // List of all implementations that match the input types - let valid_output_types = impls - .keys() - .filter(|node_io| valid_type(&node_io.call_argument, call_argument) && inputs.iter().zip(node_io.inputs.iter()).all(|(p1, p2)| valid_type(p1, p2))) + let valid_output_types = candidates + .iter() + .filter(|(node_io, _)| valid_type(&node_io.call_argument, call_argument) && inputs.iter().zip(node_io.inputs.iter()).all(|(p1, p2)| valid_type(p1, p2))) .collect::>(); // Attempt to substitute generic types with concrete types and save the list of results let substitution_results = valid_output_types .iter() - .map(|node_io| { + .map(|(node_io, constructor)| { let generics_lookup: Result, _> = collect_generics(node_io) .iter() .map(|generic| check_generic(node_io, call_argument, &inputs, generic).map(|x| (generic.to_string(), x))) .collect(); generics_lookup.map(|generics_lookup| { - let orig_node_io = (*node_io).clone(); - let mut new_node_io = orig_node_io.clone(); + let mut new_node_io = node_io.clone(); replace_generics(&mut new_node_io, &generics_lookup); - (new_node_io, orig_node_io) + (new_node_io, *constructor) }) }) .collect::>(); @@ -771,7 +779,7 @@ impl TypingContext { let convert_node_index_offset = node.original_location.auto_convert_index.unwrap_or(0); let mut best_errors = usize::MAX; let mut error_inputs = Vec::new(); - for node_io in impls.keys() { + for (node_io, _) in &candidates { // For errors on Convert nodes, offset the input index so it correctly corresponds to the node it is connected to. let current_errors = [call_argument] .into_iter() @@ -806,36 +814,36 @@ impl TypingContext { .join("\n"); Err(vec![GraphError::new(node, GraphErrorType::InvalidImplementations { inputs, error_inputs })]) } - [(node_io, org_nio)] => { + [(node_io, constructor)] => { let node_io = node_io.clone(); // Save the inferred type self.inferred.insert(node_id, node_io.clone()); - self.constructor.insert(node_id, impls[org_nio]); + self.constructor.insert(node_id, *constructor); Ok(node_io) } // If two types are available and one of them accepts () an input, always choose that one [first, second] => { if first.0.call_argument != second.0.call_argument { - for (node_io, orig_nio) in [first, second] { + for (node_io, constructor) in [first, second] { if node_io.call_argument != concrete!(()) { continue; } // Save the inferred type self.inferred.insert(node_id, node_io.clone()); - self.constructor.insert(node_id, impls[orig_nio]); + self.constructor.insert(node_id, *constructor); return Ok(node_io.clone()); } } let inputs = [call_argument].into_iter().chain(&inputs).map(ToString::to_string).collect::>().join(", "); - let valid = valid_output_types.into_iter().cloned().collect(); + let valid = valid_output_types.into_iter().map(|(node_io, _)| node_io.clone()).collect(); Err(vec![GraphError::new(node, GraphErrorType::MultipleImplementations { inputs, valid })]) } _ => { let inputs = [call_argument].into_iter().chain(&inputs).map(ToString::to_string).collect::>().join(", "); - let valid = valid_output_types.into_iter().cloned().collect(); + let valid = valid_output_types.into_iter().map(|(node_io, _)| node_io.clone()).collect(); Err(vec![GraphError::new(node, GraphErrorType::MultipleImplementations { inputs, valid })]) } } @@ -955,6 +963,81 @@ mod test { ); } + #[test] + fn retain_filter_placement_on_source_free_branch() { + let mut network = source_branch_network(vec![1], vec![]); + network.insert_context_nullification_nodes().expect("Error when calling 'insert_context_nullification_nodes'"); + + let filters = nullification_filters(&network); + assert_eq!(filters.len(), 1, "only the source-free branch gets a filter"); + let (filter_id, wrapped, retained) = &filters[0]; + assert_eq!(wrapped, "source_b"); + assert!(retained.is_empty(), "the source-free branch retains no sources"); + + let (source_a_id, _) = find_node(&network, "source_a"); + let (_, join) = find_node(&network, "join"); + let ConstructionArgs::Nodes(join_args) = &join.construction_args else { + panic!("join args must be nodes") + }; + assert_eq!(join_args, &vec![source_a_id, *filter_id], "the source branch stays direct, the filter replaces the source-free branch"); + } + + #[test] + fn diverging_source_sets_filter_each_branch() { + let mut network = source_branch_network(vec![1], vec![2]); + network.insert_context_nullification_nodes().expect("Error when calling 'insert_context_nullification_nodes'"); + + 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", &[1u64][..]), ("source_b", &[2u64][..])], + "each diverging branch is filtered down to its own source set" + ); + } + + #[test] + fn matching_source_sets_insert_no_filter() { + let mut network = source_branch_network(vec![1], vec![1]); + network.insert_context_nullification_nodes().expect("Error when calling 'insert_context_nullification_nodes'"); + + assert!(nullification_filters(&network).is_empty(), "equal branch source sets need no filter"); + } + + 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_or_else(|| panic!("node {name} not found")) + } + + 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.as_str() == graphene_core::context_modification::context_modification::IDENTIFIER.as_str()) + .map(|(id, candidate)| { + let ConstructionArgs::Nodes(args) = &candidate.construction_args else { + panic!("filter args must be nodes") + }; + let ConstructionArgs::Nodes(memoized) = &node(args[0]).construction_args else { + panic!("filter memoize args must be nodes") + }; + let ConstructionArgs::Value(value) = &node(args[1]).construction_args else { + panic!("filter payload must be a value") + }; + let value::TaggedValue::ContextModification(modification) = &**value else { + panic!("filter payload must be a context modification") + }; + (*id, node(memoized[0]).identifier.as_str().to_string(), modification.sources().to_vec()) + }) + .collect() + } + fn test_network() -> ProtoNetwork { ProtoNetwork { inputs: vec![NodeId(10)], @@ -1011,6 +1094,44 @@ mod test { } } + fn source_branch_network(branch_a_sources: Vec, branch_b_sources: Vec) -> ProtoNetwork { + let branch = |name: &str, sources: Vec| ProtoNode { + identifier: ProtoNodeIdentifier::with_owned_string(name.to_string()), + call_argument: concrete!(()), + construction_args: ConstructionArgs::Nodes(vec![NodeId(0)]), + context_features: ContextDependencies::from_sources(&sources), + ..Default::default() + }; + ProtoNetwork { + inputs: vec![], + output: NodeId(3), + nodes: [ + ( + NodeId(0), + ProtoNode { + identifier: ProtoNodeIdentifier::new("value"), + call_argument: concrete!(()), + construction_args: ConstructionArgs::Value(value::TaggedValue::U32(2).into()), + ..Default::default() + }, + ), + (NodeId(1), branch("source_a", branch_a_sources)), + (NodeId(2), branch("source_b", branch_b_sources)), + ( + NodeId(3), + ProtoNode { + identifier: ProtoNodeIdentifier::new("join"), + call_argument: concrete!(()), + construction_args: ConstructionArgs::Nodes(vec![NodeId(1), NodeId(2)]), + ..Default::default() + }, + ), + ] + .into_iter() + .collect(), + } + } + fn test_network_with_cycles() -> ProtoNetwork { ProtoNetwork { inputs: vec![NodeId(1)], diff --git a/node-graph/graphene-cli/src/export.rs b/node-graph/graphene-cli/src/export.rs index 3a1d5e1aee..68a1120a52 100644 --- a/node-graph/graphene-cli/src/export.rs +++ b/node-graph/graphene-cli/src/export.rs @@ -1,15 +1,40 @@ +use futures::executor::block_on; 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::gpoll::GPoll; +use graphene_std::core_types::ops::ConvertAsync; use graphene_std::core_types::transform::Footprint; use graphene_std::raster_types::{CPU, GPU, Raster}; use interpreted_executor::dynamic_executor::DynamicExecutor; use std::error::Error; use std::io::Cursor; use std::path::{Path, PathBuf}; +use std::sync::mpsc::Receiver; use std::time::Duration; +const SOURCE_COMPLETION_TIMEOUT: Duration = Duration::from_secs(30); + +fn execute_until_final(executor: &DynamicExecutor, render_config: RenderConfig, completion: &Receiver<()>) -> Result> { + loop { + while completion.try_recv().is_ok() {} + match executor.execute(render_config)? { + GPoll::Final(value) => return Ok(value), + GPoll::Fallback(boxed) => { + let (value, error) = *boxed; + log::warn!("Node graph evaluation reported an error alongside its fallback output: {error:?}"); + return Ok(value); + } + GPoll::Partial(_) | GPoll::Pending => { + completion + .recv_timeout(SOURCE_COMPLETION_TIMEOUT) + .map_err(|_| format!("Timed out after {}s waiting for async sources to complete", SOURCE_COMPLETION_TIMEOUT.as_secs()))?; + } + GPoll::Error(error) => return Err(format!("Node graph evaluation failed: {error:?}").into()), + } + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum FileType { Svg, @@ -28,14 +53,16 @@ pub fn detect_file_type(path: &Path) -> Result { } } -pub async fn export_document( +#[allow(clippy::too_many_arguments)] +pub fn export_document( executor: &DynamicExecutor, - wgpu_executor: &wgpu_executor::WgpuExecutor, + wgpu_executor: wgpu_executor::WgpuExecutorHandle, output_path: PathBuf, file_type: FileType, scale: f64, (width, height): (Option, Option), transparent: bool, + completion: &Receiver<()>, ) -> Result<(), Box> { // Determine export format based on file type let export_format = match file_type { @@ -57,7 +84,7 @@ pub async fn export_document( } // Execute the graph - let result = executor.execute(render_config).await?; + let result = execute_until_final(executor, render_config, completion)?; // Handle the result based on output type match result { @@ -70,7 +97,7 @@ pub async fn export_document( RenderOutputType::Texture(texture) => { // Convert GPU texture to CPU buffer let gpu_raster = Raster::::new_gpu(texture); - let cpu_raster: Raster = gpu_raster.convert(Footprint::BOUNDLESS, wgpu_executor).await; + let cpu_raster: Raster = block_on(gpu_raster.convert(Footprint::BOUNDLESS, wgpu_executor.clone())); let (data, width, height) = cpu_raster.to_flat_u8(); // Encode and write raster image @@ -149,13 +176,14 @@ impl AnimationParams { } /// Export an animated GIF by rendering multiple frames at different animation times -pub async fn export_gif( +pub fn export_gif( executor: &DynamicExecutor, - wgpu_executor: &wgpu_executor::WgpuExecutor, + wgpu_executor: wgpu_executor::WgpuExecutorHandle, output_path: PathBuf, scale: f64, (width, height): (Option, Option), animation: AnimationParams, + completion: &Receiver<()>, ) -> Result<(), Box> { use image::codecs::gif::{GifEncoder, Repeat}; use image::{Frame, RgbaImage}; @@ -195,14 +223,14 @@ pub async fn export_gif( } // Execute the graph for this frame - let result = executor.execute(render_config).await?; + let result = execute_until_final(executor, render_config, completion)?; // Extract RGBA data from result let (data, img_width, img_height) = match result { TaggedValue::RenderOutput(output) => match output.data { RenderOutputType::Texture(texture) => { let gpu_raster = Raster::::new_gpu(texture); - let cpu_raster: Raster = gpu_raster.convert(Footprint::BOUNDLESS, wgpu_executor).await; + let cpu_raster: Raster = block_on(gpu_raster.convert(Footprint::BOUNDLESS, wgpu_executor.clone())); cpu_raster.to_flat_u8() } RenderOutputType::Buffer { data, width, height } => (data, width, height), diff --git a/node-graph/graphene-cli/src/main.rs b/node-graph/graphene-cli/src/main.rs index 673b0c3aa2..a25a005c9d 100644 --- a/node-graph/graphene-cli/src/main.rs +++ b/node-graph/graphene-cli/src/main.rs @@ -7,13 +7,13 @@ 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::resource::ResourceRegistry; 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::runtime::{DynGraphRuntime, DynSpawner, GraphRuntime, RuntimeHandle, SourceFuture, Spawner, poll_once}; use interpreted_executor::dynamic_executor::DynamicExecutor; use interpreted_executor::util::wrap_network_in_scope; use std::error::Error; @@ -28,6 +28,35 @@ impl NodeGraphUpdateSender for UpdateLogger { } } +struct TokioSpawner(Option); + +impl TokioSpawner { + fn new() -> Result { + Ok(Self(Some(tokio::runtime::Runtime::new()?))) + } +} + +impl Spawner for TokioSpawner { + fn spawn(&self, mut task: SourceFuture) -> bool { + let runtime = self.0.as_ref().expect("runtime lives until drop"); + let _guard = runtime.enter(); + if poll_once(&mut task) { + return true; + } + runtime.spawn(task); + false + } +} + +/// Dropping a tokio runtime blocks on its tasks, which panics inside the async main; shut down in the background instead. +impl Drop for TokioSpawner { + fn drop(&mut self) { + if let Some(runtime) = self.0.take() { + runtime.shutdown_background(); + } + } +} + #[derive(Debug, Parser)] #[clap(name = "graphene-cli", version)] pub struct App { @@ -101,8 +130,7 @@ struct GlobalOpts { verbose: u8, } -#[tokio::main] -async fn main() -> Result<(), Box> { +fn main() -> Result<(), Box> { let app = App::parse(); let log_level = app.global_opts.verbose; @@ -130,9 +158,7 @@ async fn main() -> Result<(), Box> { let gdd = if is_gdd { let archive = std::fs::read(document_path).map_err(|error| format!("Failed to read document {}: {error}", document_path.display()))?; let container = AnyContainer::Memory(MemoryBackend::new()); - let gdd = document_format::Gdd::open_from_archive(archive.as_ref(), container, GddV1Layout) - .await - .map_err(|error| format!("Failed to open document: {error}"))?; + let gdd = block_on(document_format::Gdd::open_from_archive(archive.as_ref(), container, GddV1Layout)).map_err(|error| format!("Failed to open document: {error}"))?; Some(gdd) } else { None @@ -140,7 +166,7 @@ async fn main() -> Result<(), Box> { if let Command::ExtractLegacyDoc { ref document } = app.command { let Some(gdd) = &gdd else { return Err("ExtractLegacyDoc requires a .gdd document".into()) }; - let Some(legacy_doc) = gdd.read_legacy_document().await else { + let Some(legacy_doc) = block_on(gdd.read_legacy_document()) else { return Err("gdd file did not contain a legacy .graphite document".into()); }; let mut new_path = document.clone(); @@ -153,7 +179,7 @@ async fn main() -> Result<(), Box> { // Build the runtime network: from the `.gdd` registry, or by loading a legacy `.graphite` document. let node_network = match &gdd { Some(gdd) => { - let declarations = gdd.declarations(gdd).await; + let declarations = block_on(gdd.declarations(gdd)); let (node_network, _metadata) = gdd.registry().to_runtime_with_metadata(&declarations)?; node_network } @@ -164,7 +190,7 @@ async fn main() -> Result<(), Box> { }; log::info!("Creating GPU context"); - let mut application_io = PlatformApplicationIo::new().await; + let mut application_io = block_on(PlatformApplicationIo::new()); if let Some(gdd) = &gdd { application_io.inject_resource_proxy(Box::new(gdd.resource_proxy())); } @@ -176,16 +202,22 @@ async fn main() -> Result<(), Box> { let application_io_for_api = application_io_arc.clone(); // Get reference to wgpu executor and clone device handle - let wgpu_executor_ref = application_io_arc.gpu_executor().unwrap(); + let wgpu_executor_ref = wgpu_executor::WgpuExecutorHandle(application_io_arc.gpu_executor_arc().unwrap()); let device = wgpu_executor_ref.context().device.clone(); let preferences = EditorPreferences { max_render_region_size: EditorPreferences::default().max_render_region_size, }; + let graph_runtime: Arc = Arc::new(GraphRuntime::new(Box::new(TokioSpawner::new()?) as Box)); + let (completion_sender, completion_receiver) = std::sync::mpsc::channel(); + graph_runtime.set_notifier(Arc::new(move || { + let _ = completion_sender.send(()); + })); 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), + runtime: RuntimeHandle(graph_runtime.clone()), }); let proto_graph = compile_graph(node_network, editor_api, gdd.as_ref())?; @@ -218,7 +250,7 @@ async fn main() -> Result<(), Box> { let file_type = export::detect_file_type(&output)?; // Create executor - let executor = create_executor(proto_graph)?; + let executor = create_executor(proto_graph, graph_runtime)?; if fps <= 0. { return Err("Fps number must be positive".into()); @@ -227,9 +259,9 @@ async fn main() -> Result<(), Box> { // Perform export based on file type if file_type == export::FileType::Gif { let animation = export::AnimationParams::new(fps, frames, duration); - export::export_gif(&executor, wgpu_executor_ref, output, scale, (width, height), animation).await?; + export::export_gif(&executor, wgpu_executor_ref.clone(), output, scale, (width, height), animation, &completion_receiver)?; } else { - export::export_document(&executor, wgpu_executor_ref, output, file_type, scale, (width, height), transparent).await?; + export::export_document(&executor, wgpu_executor_ref.clone(), output, file_type, scale, (width, height), transparent, &completion_receiver)?; } } _ => unreachable!("All other commands should be handled before this match statement is run"), @@ -285,7 +317,8 @@ fn compile_graph(network: NodeNetwork, editor_api: Arc, gdd: compiler.compile_single(network).map_err(|x| x.into()) } -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())?; +fn create_executor(proto_network: ProtoNetwork, runtime: Arc) -> Result> { + let mut executor = DynamicExecutor::new(proto_network).map_err(|errors| errors.iter().map(|e| format!("{e:?}")).reduce(|acc, e| format!("{acc}\n{e}")).unwrap_or_default())?; + executor.set_runtime(runtime); Ok(executor) } diff --git a/node-graph/interpreted-executor/benches/benchmark_util.rs b/node-graph/interpreted-executor/benches/benchmark_util.rs index 54449ff13e..6b888bbafc 100644 --- a/node-graph/interpreted-executor/benches/benchmark_util.rs +++ b/node-graph/interpreted-executor/benches/benchmark_util.rs @@ -1,6 +1,5 @@ use criterion::BenchmarkGroup; use criterion::measurement::Measurement; -use futures::executor::block_on; use graph_craft::proto::ProtoNetwork; use graph_craft::util::{DEMO_ART, compile, load_from_name}; use graphene_std::application_io::EditorApi; @@ -14,10 +13,12 @@ pub fn setup_network(name: &str) -> (DynamicExecutor, ProtoNetwork) { let preprocessor = preprocessor::Preprocessor::new(); preprocessor.preprocess(&mut network, &|_| None).unwrap(); let proto_network = compile(network); - let executor = block_on(DynamicExecutor::new(proto_network.clone())).unwrap(); + let executor = DynamicExecutor::new(proto_network.clone()).unwrap(); (executor, proto_network) } +// Some benches in this module's include set drive the demos themselves. +#[allow(dead_code)] pub fn bench_for_each_demo(group: &mut BenchmarkGroup, f: F) where F: Fn(&str, &mut BenchmarkGroup), diff --git a/node-graph/interpreted-executor/benches/run_cached.rs b/node-graph/interpreted-executor/benches/run_cached.rs index e62533bd32..a692047324 100644 --- a/node-graph/interpreted-executor/benches/run_cached.rs +++ b/node-graph/interpreted-executor/benches/run_cached.rs @@ -2,6 +2,7 @@ mod benchmark_util; use benchmark_util::{bench_for_each_demo, setup_network}; use criterion::{Criterion, criterion_group, criterion_main}; +use graph_craft::graphene_compiler::Executor; use graphene_std::application_io::RenderConfig; fn subsequent_evaluations(c: &mut Criterion) { @@ -9,9 +10,7 @@ fn subsequent_evaluations(c: &mut Criterion) { let context = RenderConfig::default(); bench_for_each_demo(&mut group, |name, g| { let (executor, _) = setup_network(name); - g.bench_function(name, |b| { - b.iter(|| futures::executor::block_on(executor.tree().eval_tagged_value(executor.output(), std::hint::black_box(context))).unwrap()) - }); + g.bench_function(name, |b| b.iter(|| Executor::execute(&&executor, 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 2222b37d96..64c9a051ab 100644 --- a/node-graph/interpreted-executor/benches/run_cached_gungraun.rs +++ b/node-graph/interpreted-executor/benches/run_cached_gungraun.rs @@ -1,6 +1,7 @@ mod benchmark_util; use benchmark_util::setup_network; +use graph_craft::graphene_compiler::Executor; use graphene_std::application_io::RenderConfig; use gungraun::prelude::*; use interpreted_executor::dynamic_executor::DynamicExecutor; @@ -11,7 +12,7 @@ fn setup_run_cached(name: &str) -> DynamicExecutor { // Warm up the cache by running once let context = RenderConfig::default(); - let _ = futures::executor::block_on(executor.tree().eval_tagged_value(executor.output(), context)); + let _ = Executor::execute(&&executor, context); executor } @@ -20,7 +21,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(); - black_box(futures::executor::block_on(executor.tree().eval_tagged_value(executor.output(), black_box(context))).unwrap()); + black_box(Executor::execute(&&executor, 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 7f58565665..996e84f7fe 100644 --- a/node-graph/interpreted-executor/benches/run_demo_art_criterion.rs +++ b/node-graph/interpreted-executor/benches/run_demo_art_criterion.rs @@ -9,14 +9,11 @@ use interpreted_executor::dynamic_executor::DynamicExecutor; fn update_executor(name: &str, c: &mut BenchmarkGroup) { let network = load_from_name(name); let proto_network = compile(network); - let empty = ProtoNetwork::default(); - - let executor = futures::executor::block_on(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))), + || (DynamicExecutor::new(ProtoNetwork::default()).unwrap(), proto_network.clone()), + |(mut executor, network)| executor.update(std::hint::black_box(network)), criterion::BatchSize::SmallInput, ) }); @@ -33,10 +30,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 a7bd7ac714..9f2e368ce7 100644 --- a/node-graph/interpreted-executor/benches/run_once.rs +++ b/node-graph/interpreted-executor/benches/run_once.rs @@ -2,6 +2,7 @@ mod benchmark_util; use benchmark_util::{bench_for_each_demo, setup_network}; use criterion::{Criterion, criterion_group, criterion_main}; +use graph_craft::graphene_compiler::Executor; use graphene_std::application_io::RenderConfig; fn run_once(c: &mut Criterion) { @@ -11,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))).unwrap(), + |(executor, _)| Executor::execute(&&executor, 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 a46f5388f2..da9377b4ac 100644 --- a/node-graph/interpreted-executor/benches/run_once_gungraun.rs +++ b/node-graph/interpreted-executor/benches/run_once_gungraun.rs @@ -1,6 +1,7 @@ mod benchmark_util; use benchmark_util::setup_network; +use graph_craft::graphene_compiler::Executor; use graphene_std::application_io; use gungraun::prelude::*; use interpreted_executor::dynamic_executor::DynamicExecutor; @@ -15,7 +16,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(); - black_box(futures::executor::block_on(executor.tree().eval_tagged_value(executor.output(), black_box(context))).unwrap()); + black_box(Executor::execute(&&executor, 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 ca5f4c0a4a..081cba763a 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 91017ee3b8..42dbbe508a 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 565b775da1..536e7b6a01 100644 --- a/node-graph/interpreted-executor/src/dynamic_executor.rs +++ b/node-graph/interpreted-executor/src/dynamic_executor.rs @@ -1,17 +1,30 @@ use crate::node_registry; -use dyn_any::StaticType; +use core_types::arena::Arena; +use core_types::context::{ContextImpl, DynSlot, EvalScope, VarArg, VarArgLink, VarArgSlots}; +use core_types::gpoll::GPoll; +use core_types::node::Node; +use core_types::registry::{EdgeHandle, ErasedNode}; +use core_types::runtime::{DynGraphRuntime, DynSpawner, GraphRuntime, NoopSpawner}; use graph_craft::Type; use graph_craft::document::NodeId; -use graph_craft::document::value::{TaggedValue, UpcastAsRefNode, UpcastNode}; +use graph_craft::document::value::TaggedValue; 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, ProtoNetwork, ProtoNode, TypingContext}; use graph_craft::proto::{GraphErrorType, GraphErrors}; use std::collections::{HashMap, HashSet}; use std::error::Error; -use std::sync::Arc; +use std::sync::{Arc, Mutex, PoisonError}; + +const ARENA_CAPACITY: usize = 1 << 20; + +fn new_arena() -> Arena { + Arena::new(ARENA_CAPACITY).unwrap_or_else(|| { + log::error!("arena generations exhausted; continuing without frame caching"); + Arena::parked() + }) +} /// An executor of a node graph that does not require an online compilation server, and instead uses `Box`. -#[derive(Clone)] pub struct DynamicExecutor { output: NodeId, /// Stores all of the dynamic node structs. @@ -20,6 +33,13 @@ 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, + arena: Mutex, + runtime: Arc, + live_sources: Vec, +} + +fn noop_runtime() -> Arc { + Arc::new(GraphRuntime::new(Box::new(NoopSpawner) as Box)) } impl Default for DynamicExecutor { @@ -29,6 +49,9 @@ impl Default for DynamicExecutor { tree: Default::default(), typing_context: TypingContext::new(&node_registry::NODE_REGISTRY), orphaned_nodes: HashSet::new(), + arena: Mutex::new(new_arena()), + runtime: noop_runtime(), + live_sources: Vec::new(), } } } @@ -48,23 +71,38 @@ 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(); + let tree = BorrowTree::new(proto_network, &typing_context)?; + let runtime = noop_runtime(); + runtime.retain_sources(&sources); Ok(Self { tree, output, typing_context, orphaned_nodes: HashSet::new(), + arena: Mutex::new(new_arena()), + runtime, + live_sources: sources, }) } + pub fn set_runtime(&mut self, runtime: Arc) { + runtime.retain_sources(&self.live_sources); + self.runtime = runtime; + } + + pub fn take_dirty(&self) -> bool { + self.runtime.take_dirty() + } + /// 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 +125,10 @@ impl DynamicExecutor { (ResolvedDocumentNodeTypesDelta { add, remove: Vec::new() }, e) })?; - let (add, orphaned) = self - .tree - .update(proto_network, &self.typing_context) - .await - .map_err(|e| (ResolvedDocumentNodeTypesDelta::default(), e))?; + let sources = proto_network.source_ids(); + let (add, orphaned) = self.tree.update(proto_network, &self.typing_context).map_err(|e| (ResolvedDocumentNodeTypesDelta::default(), e))?; + self.runtime.retain_sources(&sources); + self.live_sources = sources; 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 { @@ -135,27 +172,50 @@ impl DynamicExecutor { } } -impl Executor for &DynamicExecutor +impl Executor> for &DynamicExecutor where - I: StaticType + 'static + Send + Sync + std::panic::UnwindSafe, + I: VarArg + Send + Sync + std::panic::RefUnwindSafe, { - 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> { + let Some(handle) = self.tree.get(self.output) else { + return Err("Output node not found in executor".into()); + }; + let mut arena = self.arena.lock().unwrap_or_else(PoisonError::into_inner); + let result = eval_root(&mut arena, &self.runtime, &input, |ctx| match TaggedValue::from_edge(handle.duplicate(), ctx) { + Ok(poll) => poll.map(Ok), + Err(error) => GPoll::Final(Err(error)), + }); + match result { + GPoll::Final(value) => Ok(GPoll::Final(value?)), + GPoll::Partial(value) => Ok(GPoll::Partial(value?)), + GPoll::Fallback(boxed) => { + let (value, error) = *boxed; + Ok(GPoll::Fallback(Box::new((value?, error)))) } - }) + GPoll::Pending => Ok(GPoll::Pending), + GPoll::Error(error) => Ok(GPoll::Error(error)), + } } } +pub fn eval_root(arena: &mut Arena, runtime: &GraphRuntime, call_argument: DynSlot, eval: impl FnOnce(&ContextImpl) -> GPoll) -> GPoll { + arena.reset(); + let generations = runtime.snapshot(); + let scope = EvalScope::new(None, None, None, &generations, arena); + let root = ContextImpl::root(&scope); + let link = VarArgLink { + args: VarArgSlots::Single(call_argument), + outer: None, + }; + let ctx = root.with_varargs(&link); + match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| eval(&ctx))) { + Ok(result) => result, + Err(_) => { + arena.reset(); + GPoll::panicked() + } + } +} + pub struct InputMapping {} #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -188,39 +248,39 @@ impl std::fmt::Display for IntrospectError { /// /// # Fields /// -/// * `nodes`: A [`HashMap`] of [`NodeId`]s to tuples of [`SharedNodeContainer`] and [`Path`]. +/// * `nodes`: A [`HashMap`] of [`NodeId`]s to tuples of [`EdgeHandle`] and [`Path`]. /// This stores the actual node instances and their associated paths. /// /// * `source_map`: A [`HashMap`] from [`Path`] to tuples of [`NodeId`] and [`NodeTypes`]. /// This maps document paths to node IDs and their associated type information. /// /// A store of the dynamically typed nodes and also the source map. -#[derive(Default, Clone)] +#[derive(Default)] pub struct BorrowTree { /// A hashmap of node IDs and dynamically typed nodes. - nodes: HashMap, + 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()); } @@ -229,44 +289,33 @@ impl BorrowTree { Ok((new_nodes, old_nodes)) } - fn node_deps(&self, nodes: &[NodeId]) -> Vec { - nodes.iter().map(|node| self.nodes.get(node).unwrap().0.clone()).collect() + fn node_deps(&self, nodes: &[NodeId]) -> Vec { + nodes.iter().map(|node| self.nodes.get(node).unwrap().0.duplicate()).collect() } - fn store_node(&mut self, node: SharedNodeContainer, id: NodeId, path: Path) { + fn store_node(&mut self, node: EdgeHandle, id: NodeId, path: Path) { self.nodes.insert(id, (node, path)); } - /// 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. + /// Calls the `Node::serialize` for that specific node, returning for example the captured io record 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))?; node.serialize().ok_or(IntrospectError::NoData) } - pub fn get(&self, id: NodeId) -> Option { - self.nodes.get(&id).map(|(node, _)| node.clone()) + pub fn get(&self, id: NodeId) -> Option { + self.nodes.get(&id).map(|(node, _)| node.duplicate()) } - /// Evaluate the output node of the [`BorrowTree`]. - pub async fn eval<'i, I, O>(&'i self, id: NodeId, input: I) -> Option + /// Evaluate a node of the [`BorrowTree`], downcasting its edge to the expected output type. + pub fn eval(&self, id: NodeId, input: &I) -> Option> where - I: StaticType + 'i + Send + Sync, - O: StaticType + 'i, + ErasedNode: Node, { - let (node, _path) = self.nodes.get(&id).cloned()?; - let output = node.eval(Box::new(input)); - dyn_any::downcast::(output.await).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 - where - I: StaticType + 'static + Send + Sync + std::panic::UnwindSafe, - { - 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)?; + let edge = node.duplicate().downcast::().ok()?; + Some(edge.eval(input)) } /// Removes a node from the [`BorrowTree`] and returns its associated path. @@ -293,10 +342,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 typing_context = TypingContext::default(); + /// let mut borrow_tree = BorrowTree::new(proto_network, &typing_context)?; /// /// // Assert that the node exists in the BorrowTree /// assert!(borrow_tree.get(node_id).is_some(), "Node should exist before removal"); @@ -393,30 +442,23 @@ 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) - }; + let node = (**value) + .clone() + .to_edge() + .map_err(|error| vec![GraphError::new(&proto_node, GraphErrorType::ConstructionFailed(error))])?; self.store_node(node, id, path.into()); } ConstructionArgs::Inline(_) => unimplemented!("Inline nodes are not supported yet"), ConstructionArgs::Nodes(ids) => { - let ids = ids.to_vec(); - let construction_nodes = self.node_deps(&ids); + let construction_nodes = self.node_deps(ids); let constructor = typing_context.constructor(id).ok_or_else(|| vec![GraphError::new(&proto_node, GraphErrorType::NoConstructor)])?; - let node = constructor(construction_nodes).await; - let node = NodeContainer::new(node); + let node = constructor(construction_nodes).map_err(|error| vec![GraphError::new(&proto_node, GraphErrorType::ConstructionFailed(format!("{error:?}")))])?; self.store_node(node, id, path.into()); } }; @@ -432,17 +474,76 @@ impl BorrowTree { #[cfg(test)] mod test { use super::*; + use core_types::arena::ArenaCell; + use core_types::context::{ExtractFootprint, ExtractVarArgs}; + use core_types::runtime::{SourceFuture, Spawner}; use graph_craft::document::value::TaggedValue; + struct InertSpawner; + + impl Spawner for InertSpawner { + fn spawn(&self, _task: SourceFuture) -> bool { + false + } + } + + #[test] + fn eval_root_builds_the_bare_root_with_the_call_argument_as_vararg_0() { + let mut arena = Arena::new(64).unwrap(); + let runtime = GraphRuntime::new(InertSpawner); + let argument = 21.5f64; + let result = eval_root(&mut arena, &runtime, &argument, |ctx| { + assert!(ctx.try_footprint().is_none(), "the bare root carries no axes"); + GPoll::Final(ctx.vararg(0).ok().and_then(|slot| slot.downcast_ref::()).copied().unwrap_or(0.)) + }); + assert_eq!(result, GPoll::Final(21.5)); + } + + #[test] + fn eval_root_resets_the_arena_at_eval_start() { + let mut arena = Arena::new(64).unwrap(); + let runtime = GraphRuntime::new(InertSpawner); + let cell = ArenaCell::new(); + eval_root(&mut arena, &runtime, &(), |ctx| { + let (_, weak) = ctx.scope().arena().alloc(5u32).unwrap(); + cell.store(weak); + GPoll::Final(()) + }); + assert!(cell.load(&arena).is_some(), "the introspection window spans until the next eval"); + eval_root(&mut arena, &runtime, &(), |ctx| { + assert!(cell.load(ctx.scope().arena()).is_none(), "the reset at eval start reclaims the previous frame"); + GPoll::Final(()) + }); + } + + #[test] + fn a_panicking_eval_reports_the_error_and_resets_the_arena() { + let mut arena = Arena::new(64).unwrap(); + let runtime = GraphRuntime::new(InertSpawner); + let cell = ArenaCell::new(); + let result: GPoll<()> = eval_root(&mut arena, &runtime, &(), |ctx| { + let (_, weak) = ctx.scope().arena().alloc(5u32).unwrap(); + cell.store(weak); + panic!("mid-eval"); + }); + assert_eq!(result, GPoll::panicked()); + assert!(cell.load(&arena).is_none(), "reset-on-panic leaves no stale records"); + assert_eq!(eval_root(&mut arena, &runtime, &(), |_| GPoll::Final(7u32)), GPoll::Final(7)); + } + #[test] fn push_node_sync() { let mut tree = BorrowTree::default(); let val_1_protonode = ProtoNode::value(ConstructionArgs::Value(TaggedValue::U32(2u32).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 = futures::executor::block_on(tree.eval(NodeId(0), ())); - assert_eq!(result, Some(2u32)); + + let arena = Arena::new(64).unwrap(); + let generations = []; + let scope = EvalScope::new(None, None, None, &generations, &arena); + let ctx = ContextImpl::root(&scope); + let result: Option> = tree.eval(NodeId(0), &ctx); + assert_eq!(result, Some(GPoll::Final(2))); } } diff --git a/node-graph/interpreted-executor/src/lib.rs b/node-graph/interpreted-executor/src/lib.rs index 68081de5a8..44fca1526a 100644 --- a/node-graph/interpreted-executor/src/lib.rs +++ b/node-graph/interpreted-executor/src/lib.rs @@ -5,7 +5,6 @@ pub mod util; #[cfg(test)] mod tests { use core_types::*; - use futures::executor::block_on; use graphene_core::ops::passthrough; #[test] @@ -47,6 +46,6 @@ mod tests { let compiler = Compiler {}; let protograph = compiler.compile_single(network).expect("Graph should be generated"); - let _exec = block_on(DynamicExecutor::new(protograph)).map(|_e| panic!("The network should not type check ")).unwrap_err(); + let _exec = DynamicExecutor::new(protograph).map(|_e| panic!("The network should not type check ")).unwrap_err(); } } diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index d9ee2e6ad2..f48005aafe 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -1,10 +1,7 @@ -use dyn_any::StaticType; use glam::{DAffine2, DVec2, IVec2}; use graph_craft::application_io::PlatformEditorApi; use graph_craft::document::DocumentNode; use graph_craft::document::value::RenderOutput; -use graph_craft::proto::{NodeConstructor, TypeErasedBox}; -use graphene_std::any::DynAnyNode; use graphene_std::application_io::Texture; use graphene_std::brush::brush_stroke::BrushStroke; use graphene_std::gradient::GradientStops; @@ -16,19 +13,20 @@ use graphene_std::raster::GPU; use graphene_std::raster::color::Color; use graphene_std::raster::*; use graphene_std::raster::{CPU, Raster}; +use graphene_std::registry::{ConstructionError, EdgeHandle, ErasedNode, NodeIOTypes, RegistryEntry}; use graphene_std::render_node::RenderIntermediate; +use graphene_std::runtime::RuntimeHandle; use graphene_std::transform::Footprint; use graphene_std::uuid::NodeId; use graphene_std::vector::Vector; -use graphene_std::{Artboard, Context, Graphic, NodeIO, NodeIOTypes, ProtoNodeIdentifier, concrete, fn_type_fut, future}; +use graphene_std::{Artboard, Context, Graphic, ProtoNodeIdentifier, SourceId, concrete, fn_type}; use node_registry_macros::{async_node, convert_node, into_node}; use std::collections::HashMap; #[cfg(feature = "gpu")] -use wgpu_executor::WgpuExecutor; +use wgpu_executor::WgpuExecutorHandle; -// TODO: turn into hashmap -fn node_registry() -> HashMap> { - let mut node_types: Vec<(ProtoNodeIdentifier, NodeConstructor, NodeIOTypes)> = vec![ +fn node_registry() -> HashMap> { + let mut node_types: Vec<(ProtoNodeIdentifier, RegistryEntry)> = vec![ // ========== // INTO NODES // ========== @@ -92,8 +90,6 @@ fn node_registry() -> HashMap>, to: AttributeValueDyn), convert_node!(from: List, to: AttributeValueDyn), // into_node!(from: List>, to: List>), - #[cfg(feature = "gpu")] - into_node!(from: &PlatformEditorApi, to: &WgpuExecutor), convert_node!(from: DVec2, to: DVec2), convert_node!(from: List, to: List), convert_node!(from: DVec2, to: List), @@ -103,111 +99,118 @@ fn node_registry() -> HashMap>, to: List>, converter: &WgpuExecutor), + convert_node!(from: List>, to: List>, converter: WgpuExecutorHandle), #[cfg(feature = "gpu")] - convert_node!(from: List>, to: List>, converter: &WgpuExecutor), + convert_node!(from: List>, to: List>, converter: WgpuExecutorHandle), #[cfg(feature = "gpu")] - convert_node!(from: List>, to: List>, converter: &WgpuExecutor), + convert_node!(from: List>, to: List>, converter: WgpuExecutorHandle), #[cfg(feature = "gpu")] - convert_node!(from: List>, to: List>, converter: &WgpuExecutor), + convert_node!(from: List>, to: List>, converter: WgpuExecutorHandle, async), // ============= // MONITOR NODES // ============= - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => ()]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List>]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => ()]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => List]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => List]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => List]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => List>]), #[cfg(feature = "gpu")] - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List>]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Image]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => String]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => IVec2]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => DVec2]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => DAffine2]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Option]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => bool]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => f64]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => u32]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => u64]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => BlendMode]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Texture]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::application_io::resource::Resource]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::transform::ReferencePoint]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::BooleanOperation]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::StrokeCap]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::StrokeJoin]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::PaintOrder]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::StrokeAlign]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::Stroke]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Box]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::CentroidType]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::PointSpacingType]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Option]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => AttributeDyn]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => AttributeValueDyn]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => ListDyn]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Graphic]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::text::Font]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => DocumentNode]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::transform::Footprint]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::blending::BlendMode]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::LuminanceCalculation]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::extract_xy::XY]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::text_nodes::StringCapitalization]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::RedGreenBlue]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::RedGreenBlueAlpha]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::animation::RealTimeMode]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::NoiseType]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::FractalType]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::CellularDistanceFunction]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::CellularReturnType]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::DomainWarpType]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::RelativeAbsolute]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::SelectiveColorChoice]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::GridType]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::ArcType]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::RowsOrColumns]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::MergeByDistanceAlgorithm]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::ExtrudeJoiningAlgorithm]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::PointSpacingType]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::GradientType]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::GradientSpreadMethod]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::transform::ReferencePoint]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::CentroidType]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::text::TextAlign]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::transform::ScaleType]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::InterpolationDistribution]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => List>]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => List]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => List]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => Image]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => String]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => IVec2]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => DVec2]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => DAffine2]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => Option]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => bool]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => f64]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => u32]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => u64]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => BlendMode]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => Texture]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::application_io::resource::Resource]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::transform::ReferencePoint]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::BooleanOperation]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::StrokeCap]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::StrokeJoin]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::PaintOrder]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::StrokeAlign]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::Stroke]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => Box]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::CentroidType]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::PointSpacingType]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => Option]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => List]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => List]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => List]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => List]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => List]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => List]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => List]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => List]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => List]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => AttributeDyn]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => AttributeValueDyn]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => ListDyn]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => Graphic]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::text::Font]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => List]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => DocumentNode]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::transform::Footprint]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::blending::BlendMode]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::LuminanceCalculation]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::extract_xy::XY]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::text_nodes::StringCapitalization]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::RedGreenBlue]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::RedGreenBlueAlpha]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::animation::RealTimeMode]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::NoiseType]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::FractalType]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::CellularDistanceFunction]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::CellularReturnType]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::DomainWarpType]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::RelativeAbsolute]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::SelectiveColorChoice]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::GridType]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::ArcType]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::RowsOrColumns]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::MergeByDistanceAlgorithm]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::ExtrudeJoiningAlgorithm]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::PointSpacingType]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::GradientType]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::GradientSpreadMethod]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::transform::ReferencePoint]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::CentroidType]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::text::TextAlign]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::transform::ScaleType]), + async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::InterpolationDistribution]), // Context nullification #[cfg(feature = "gpu")] - async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => &PlatformEditorApi, Context => graphene_std::ContextFeatures]), - async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => RenderIntermediate, Context => graphene_std::ContextFeatures]), - async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => RenderOutput, Context => graphene_std::ContextFeatures]), - async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => AttributeDyn, Context => graphene_std::ContextFeatures]), - async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => AttributeValueDyn, Context => graphene_std::ContextFeatures]), - async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => ListDyn, Context => graphene_std::ContextFeatures]), + async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => std::sync::Arc, Context => graphene_std::ContextModification]), + async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => RuntimeHandle, Context => graphene_std::ContextModification]), + async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => SourceId, Context => graphene_std::ContextModification]), + async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => RenderIntermediate, Context => graphene_std::ContextModification]), + async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => RenderOutput, Context => graphene_std::ContextModification]), + async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => AttributeDyn, Context => graphene_std::ContextModification]), + async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => AttributeValueDyn, Context => graphene_std::ContextModification]), + async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => ListDyn, Context => graphene_std::ContextModification]), + async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::GradientType, Context => graphene_std::ContextModification]), + async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::GradientSpreadMethod, Context => graphene_std::ContextModification]), + async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Option, Context => graphene_std::ContextModification]), #[cfg(target_family = "wasm")] - async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => CanvasHandle, Context => graphene_std::ContextFeatures]), - async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => &PlatformEditorApi, Context => graphene_std::ContextFeatures]), - async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => &wgpu_executor::WgpuExecutor, Context => graphene_std::ContextFeatures]), - async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Option<&wgpu_executor::WgpuExecutor>, Context => graphene_std::ContextFeatures]), - async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => wgpu_executor::WgpuPipelineCache, Context => graphene_std::ContextFeatures]), + async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => CanvasHandle, Context => graphene_std::ContextModification]), + async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => std::sync::Arc, Context => graphene_std::ContextModification]), + async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => wgpu_executor::WgpuExecutorHandle, Context => graphene_std::ContextModification]), + async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Option, Context => graphene_std::ContextModification]), + async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => wgpu_executor::WgpuPipelineCache, Context => graphene_std::ContextModification]), // ========== // MEMO NODES // ========== async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => ()]), + async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => RuntimeHandle]), + async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => SourceId]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => bool]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List]), @@ -239,7 +242,7 @@ fn node_registry() -> HashMap, input: Context, fn_params: [Context => DAffine2]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Footprint]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => RenderOutput]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => &PlatformEditorApi]), + async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => std::sync::Arc]), #[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 => Option]), @@ -251,7 +254,7 @@ fn node_registry() -> HashMap, input: Context, fn_params: [Context => graphene_std::text::Font]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => DocumentNode]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::ContextFeatures]), + async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::ContextModification]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::transform::Footprint]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Box]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::blending::BlendMode]), @@ -280,6 +283,8 @@ fn node_registry() -> HashMap, input: Context, fn_params: [Context => graphene_std::vector::style::StrokeAlign]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::PaintOrder]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::GradientType]), + async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::GradientSpreadMethod]), + async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Option]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::transform::ReferencePoint]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::CentroidType]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::BooleanOperation]), @@ -287,8 +292,8 @@ fn node_registry() -> HashMap, input: Context, fn_params: [Context => graphene_std::transform::ScaleType]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::InterpolationDistribution]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => RenderIntermediate]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => &wgpu_executor::WgpuExecutor]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Option<&wgpu_executor::WgpuExecutor>]), + async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => wgpu_executor::WgpuExecutorHandle]), + async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Option]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => wgpu_executor::WgpuPipelineCache]), ]; // ============= @@ -317,15 +322,21 @@ fn node_registry() -> HashMap> = HashMap::new(); + let mut map: HashMap> = HashMap::new(); + let insert = |map: &mut HashMap>, id: ProtoNodeIdentifier, entry: RegistryEntry| { + let rows = map.entry(id).or_default(); + if !rows.iter().any(|row| row.io == entry.io) { + rows.push(entry); + } + }; - for (id, entry) in graphene_std::registry::NODE_REGISTRY.lock().unwrap().iter() { - for (constructor, types) in entry.iter() { - map.entry(id.clone()).or_default().insert(types.clone(), *constructor); + for (id, entries) in graphene_std::registry::NODE_REGISTRY.lock().unwrap().iter() { + for entry in entries { + insert(&mut map, id.clone(), entry.clone()); } } - for (id, node_constructor, types) in node_types.into_iter() { + for (id, entry) in node_types.into_iter() { // TODO: this is a hack to remove the newline from the node new_name // This occurs for the ChannelMixerNode presumably because of the long name. // This might be caused by the stringify! macro @@ -338,43 +349,35 @@ fn node_registry() -> HashMap) or similar -pub static NODE_REGISTRY: once_cell::sync::Lazy>> = once_cell::sync::Lazy::new(|| node_registry()); +pub static NODE_REGISTRY: once_cell::sync::Lazy>> = once_cell::sync::Lazy::new(node_registry); mod node_registry_macros { macro_rules! async_node { - // TODO: we currently need to annotate the type here because the compiler would otherwise (correctly) - // TODO: assign a Pin>> type to the node, which is not what we want for now. - // // This `params` variant of the macro wraps the normal `fn_params` variant and is used as a shorthand for writing `T` instead of `() => T` ($path:ty, input: $input:ty, params: [$($type:ty),*]) => { async_node!($path, input: $input, fn_params: [ $(() => $type),*]) }; - ($path:ty, input: $input:ty, fn_params: [$($arg:ty => $type:ty),*]) => { + ($path:ty, input: $input:ty, fn_params: [$first_arg:ty => $first:ty $(, $arg:ty => $type:ty)*]) => { ( ProtoNodeIdentifier::new(stringify!($path)), - |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 - }) - }, - { - let node = <$path>::new($( - graphene_std::any::PanicNode::<$arg, core::pin::Pin + Send>>>::new() - ),*); - let params = vec![$(fn_type_fut!($arg, $type)),*]; - let mut node_io = NodeIO::<'_, $input>::to_async_node_io(&node, params); - node_io.call_argument = concrete!(<$input as StaticType>::Static); - node_io + RegistryEntry { + io: NodeIOTypes::new(concrete!($input), concrete!($first), vec![fn_type!($first_arg, $first) $(, fn_type!($arg, $type))*]), + constructor: |inputs| { + let expected = [stringify!($first) $(, stringify!($type))*].len(); + if inputs.len() != expected { + return Err(ConstructionError::Arity { expected, got: inputs.len() }); + } + let mut inputs = inputs.into_iter(); + let node = <$path>::new(inputs.next().unwrap().downcast::<$first>()? $(, inputs.next().unwrap().downcast::<$type>()?)*); + Ok(EdgeHandle::new(std::sync::Arc::new(node) as std::sync::Arc>)) + }, }, ) }; @@ -384,31 +387,23 @@ mod node_registry_macros { (from: $from:ty, to: $to:ty) => { ( ProtoNodeIdentifier::new(concat!["graphene_core::ops::IntoNode<", stringify!($to), ">"]), - |mut args| { - Box::pin(async move { - let node = graphene_std::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_std::ops::IntoNode::new( - graphene_std::any::PanicNode:: + Send>>>::new(), - graphene_std::any::FutureWrapperNode::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); - node_io + RegistryEntry { + io: NodeIOTypes::new(concrete!(Context), concrete!($to), vec![fn_type!(Context, $from)]), + constructor: |inputs| { + if inputs.len() != 1 { + return Err(ConstructionError::Arity { expected: 1, got: inputs.len() }); + } + let mut inputs = inputs.into_iter(); + let node = graphene_std::ops::IntoNode::<$to, _>::new(inputs.next().unwrap().downcast::<$from>()?); + Ok(EdgeHandle::new(std::sync::Arc::new(node) as std::sync::Arc>)) + }, }, ) }; } macro_rules! convert_node { (from: $from:ty, to: numbers) => {{ - let x: Vec<(ProtoNodeIdentifier, NodeConstructor, NodeIOTypes)> = vec![ + let x: Vec<(ProtoNodeIdentifier, RegistryEntry)> = vec![ convert_node!(from: $from, to: f32), convert_node!(from: $from, to: f64), convert_node!(from: $from, to: i8), @@ -427,7 +422,7 @@ mod node_registry_macros { x }}; (from: numbers, to: $to:ty) => {{ - let x: Vec<(ProtoNodeIdentifier, NodeConstructor, NodeIOTypes)> = vec![ + let x: Vec<(ProtoNodeIdentifier, RegistryEntry)> = vec![ convert_node!(from: f32, to: $to), convert_node!(from: f64, to: $to), convert_node!(from: i8, to: $to), @@ -448,31 +443,44 @@ mod node_registry_macros { (from: $from:ty, to: $to:ty) => { convert_node!(from: $from, to: $to, converter: ()) }; - (from: $from:ty, to: $to:ty, converter: $convert:ty) => { + (from: $from:ty, to: $to:ty, converter: $convert:ty, async) => { ( ProtoNodeIdentifier::new(concat!["graphene_core::ops::ConvertNode<", stringify!($to), ">"]), - |mut args| { - Box::pin(async move { - let mut args = args.drain(..); - let node = graphene_std::ops::ConvertNode::new( - graphene_std::any::downcast_node::(args.next().expect("Convert node did not get first argument")), - graphene_std::any::downcast_node::(args.next().expect("Convert node did not get converter argument")), - graphene_std::any::FutureWrapperNode::new(graphene_std::value::ClonedNode::new(std::marker::PhantomData::<$to>)) + RegistryEntry { + io: NodeIOTypes::new( + concrete!(Context), + concrete!($to), + vec![fn_type!(Context, $from), fn_type!(Context, $convert), fn_type!(Context, RuntimeHandle), fn_type!(Context, SourceId)], + ), + constructor: |inputs| { + if inputs.len() != 4 { + return Err(ConstructionError::Arity { expected: 4, got: inputs.len() }); + } + let mut inputs = inputs.into_iter(); + let node = graphene_std::ops::ConvertAsyncNode::<$to, _, _, _, _>::new( + inputs.next().unwrap().downcast::<$from>()?, + inputs.next().unwrap().downcast::<$convert>()?, + inputs.next().unwrap().downcast::()?, + inputs.next().unwrap().downcast::()?, ); - let any: DynAnyNode = graphene_std::any::DynAnyNode::new(node); - Box::new(any) as TypeErasedBox - }) + Ok(EdgeHandle::new(std::sync::Arc::new(node) as std::sync::Arc>)) + }, }, - { - let node = graphene_std::ops::ConvertNode::new( - - graphene_std::any::PanicNode:: + Send>>>::new(), - graphene_std::any::PanicNode:: + Send>>>::new(), - graphene_std::any::FutureWrapperNode::new(graphene_std::value::ClonedNode::new(std::marker::PhantomData::<$to>)) - ); - let params = vec![fn_type_fut!(Context, $from), fn_type_fut!(Context, $convert)]; - let node_io = NodeIO::<'_, Context>::to_async_node_io(&node, params); - node_io + ) + }; + (from: $from:ty, to: $to:ty, converter: $convert:ty) => { + ( + ProtoNodeIdentifier::new(concat!["graphene_core::ops::ConvertNode<", stringify!($to), ">"]), + RegistryEntry { + io: NodeIOTypes::new(concrete!(Context), concrete!($to), vec![fn_type!(Context, $from), fn_type!(Context, $convert)]), + constructor: |inputs| { + if inputs.len() != 2 { + return Err(ConstructionError::Arity { expected: 2, got: inputs.len() }); + } + let mut inputs = inputs.into_iter(); + let node = graphene_std::ops::ConvertNode::<$to, _, _>::new(inputs.next().unwrap().downcast::<$from>()?, inputs.next().unwrap().downcast::<$convert>()?); + Ok(EdgeHandle::new(std::sync::Arc::new(node) as std::sync::Arc>)) + }, }, ) }; diff --git a/node-graph/interpreted-executor/src/util.rs b/node-graph/interpreted-executor/src/util.rs index 670f00d397..b99f2ce9cc 100644 --- a/node-graph/interpreted-executor/src/util.rs +++ b/node-graph/interpreted-executor/src/util.rs @@ -30,20 +30,14 @@ pub fn wrap_network_in_scope(network: NodeNetwork, editor_api: Arc)))]; NodeNetwork { exports: vec![NodeInput::node(NodeId(1), 0)], diff --git a/node-graph/libraries/application-io/Cargo.toml b/node-graph/libraries/application-io/Cargo.toml index 26170f2262..5e4beb61c5 100644 --- a/node-graph/libraries/application-io/Cargo.toml +++ b/node-graph/libraries/application-io/Cargo.toml @@ -16,6 +16,7 @@ wgpu = ["dep:raster-types", "raster-types/wgpu"] # Local dependencies dyn-any = { workspace = true } core-types = { workspace = true } +graphene-hash = { workspace = true } vector-types = { workspace = true } text-nodes = { workspace = true } graphene-resource = { workspace = true } diff --git a/node-graph/libraries/application-io/src/lib.rs b/node-graph/libraries/application-io/src/lib.rs index 1b5fbe3115..80a2012b17 100644 --- a/node-graph/libraries/application-io/src/lib.rs +++ b/node-graph/libraries/application-io/src/lib.rs @@ -21,6 +21,9 @@ pub trait ApplicationIo { fn gpu_executor(&self) -> Option<&Self::Executor> { None } + fn gpu_executor_arc(&self) -> Option> { + None + } fn load_resource(&self, hash: resource::ResourceHash) -> resource::ResourceFuture<'_>; } @@ -31,6 +34,10 @@ impl ApplicationIo for &T { (**self).gpu_executor() } + fn gpu_executor_arc(&self) -> Option> { + (**self).gpu_executor_arc() + } + fn load_resource(&self, hash: resource::ResourceHash) -> resource::ResourceFuture<'_> { (**self).load_resource(hash) } @@ -54,7 +61,7 @@ pub trait GetEditorPreferences { fn max_render_region_area(&self) -> u32; } -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, graphene_hash::CacheHash)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum ExportFormat { #[default] @@ -62,14 +69,14 @@ pub enum ExportFormat { Raster, } -#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny)] +#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny, graphene_hash::CacheHash)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct TimingInformation { pub time: f64, pub animation_time: Duration, } -#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny)] +#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny, graphene_hash::CacheHash)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct RenderConfig { pub viewport: Footprint, @@ -105,6 +112,7 @@ pub struct EditorApi { pub node_graph_message_sender: Box, /// Editor preferences made available to the graph through the `PlatformEditorApi`. pub editor_preferences: Box, + pub runtime: core_types::runtime::RuntimeHandle, } impl Eq for EditorApi {} @@ -115,6 +123,7 @@ impl Default for EditorApi { application_io: None, node_graph_message_sender: Box::new(Logger), editor_preferences: Box::new(DummyPreferences), + runtime: Default::default(), } } } diff --git a/node-graph/libraries/core-types/src/arena.rs b/node-graph/libraries/core-types/src/arena.rs new file mode 100644 index 0000000000..e5fdaf1e10 --- /dev/null +++ b/node-graph/libraries/core-types/src/arena.rs @@ -0,0 +1,426 @@ +use std::cell::UnsafeCell; +use std::marker::PhantomData; +use std::mem::MaybeUninit; +use std::sync::Mutex; +use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering}; + +/// Handle word layout: 24 generation bits above 40 offset bits, so a 1 TiB arena is +/// addressable and generations run out after ~3 days of 60fps resets. +const OFFSET_BITS: u32 = 40; +const OFFSET_MASK: u64 = (1 << OFFSET_BITS) - 1; +const GENERATION_MASK: u64 = (1 << (64 - OFFSET_BITS)) - 1; + +/// Out of the encodable range, so no handle, including `NULL`, matches it. +const PARKED_GENERATION: u64 = GENERATION_MASK + 1; + +pub struct Arena { + generation: AtomicU64, + offset: AtomicUsize, + buf: Box<[UnsafeCell>]>, + drops: Mutex>, +} + +impl std::fmt::Debug for Arena { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Arena").field("generation", &self.generation).field("size", &self.buf.len()).finish() + } +} + +struct DropEntry { + offset: usize, + drop_fn: unsafe fn(*mut u8), +} + +// SAFETY: disjoint regions are handed out by an atomic bump; a region is written +// only by its allocating caller before publication, and cross-thread hand-off is +// ordered by the Release/Acquire pair on the published handle word. Payloads are +// `Send + Sync` by the bound on every allocating method. +unsafe impl Sync for Arena {} +unsafe impl Send for Arena {} + +impl std::panic::UnwindSafe for Arena {} +impl std::panic::RefUnwindSafe for Arena {} + +/// Shared by all arenas, so a foreign handle misses like a stale one. +static NEXT_GENERATION: AtomicU64 = AtomicU64::new(1); + +static LIVE_ARENAS: AtomicUsize = AtomicUsize::new(0); + +/// `None` past [`GENERATION_MASK`], where a reissued generation would let an ancient +/// handle upgrade against a current arena. Recovered by `reset_generation_counter`. +fn next_generation() -> Option { + let generation = NEXT_GENERATION.fetch_add(1, Ordering::Relaxed); + (generation <= GENERATION_MASK).then_some(generation) +} + +/// Rewinds the shared generation counter so previously issued values are reused. +/// Returns `false` without rewinding when an [`Arena`] is live at the time of the +/// check, which is a debugging aid rather than a guarantee. +/// +/// # Safety +/// +/// No [`ArenaWeak`] minted before this call may be upgraded afterwards. Dropping +/// every [`Arena`] is not sufficient, since nodes also hold handles in +/// [`ArenaCell`]s; those nodes must be dropped too. No arena may be constructed +/// concurrently either, since the live check and the rewind are separate steps. +pub unsafe fn reset_generation_counter() -> bool { + if LIVE_ARENAS.load(Ordering::Acquire) != 0 { + return false; + } + NEXT_GENERATION.store(1, Ordering::Release); + true +} + +impl Arena { + pub fn new(capacity: usize) -> Option { + let generation = next_generation()?; + let buf = (0..capacity).map(|_| UnsafeCell::new(MaybeUninit::uninit())).collect(); + LIVE_ARENAS.fetch_add(1, Ordering::Release); + Some(Self { + generation: AtomicU64::new(generation), + offset: AtomicUsize::new(0), + buf, + drops: Mutex::new(Vec::new()), + }) + } + + /// An arena that refuses every allocation and resolves no handle, so a caller that + /// cannot fail can degrade instead of propagating exhaustion. + pub fn parked() -> Self { + LIVE_ARENAS.fetch_add(1, Ordering::Release); + Self { + generation: AtomicU64::new(PARKED_GENERATION), + offset: AtomicUsize::new(0), + buf: Box::new([]), + drops: Mutex::new(Vec::new()), + } + } + + pub fn generation(&self) -> u64 { + self.generation.load(Ordering::Acquire) + } + + fn base(&self) -> *mut u8 { + self.buf.as_ptr() as *mut u8 + } + + fn reserve(&self, size: usize, align: usize) -> Option { + debug_assert!(align.is_power_of_two()); + let base = self.base() as usize; + let mut start = 0; + self.offset + .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| { + // Alignment is computed on the absolute address; the backbone + // allocation itself has no alignment guarantee. + let addr = (base.checked_add(current)?.checked_add(align - 1)?) & !(align - 1); + start = addr - base; + let end = start.checked_add(size)?; + (end <= self.buf.len()).then_some(end) + }) + .ok()?; + Some(start) + } + + pub fn alloc(&self, value: T) -> Option<(&T, ArenaWeak)> { + let offset = self.reserve(size_of::(), align_of::())?; + // Built before the write so an unencodable offset drops `value` here + // rather than stranding it in the arena without drop glue. + let weak = ArenaWeak::new(self.generation(), offset)?; + let ptr = unsafe { self.base().add(offset) }.cast::(); + // SAFETY: freshly reserved, aligned, in-bounds, unaliased. + unsafe { ptr.write(value) }; + if std::mem::needs_drop::() { + unsafe fn glue(p: *mut u8) { + unsafe { p.cast::().drop_in_place() } + } + self.drops.lock().unwrap().push(DropEntry { offset, drop_fn: glue:: }); + } + // SAFETY: initialized above; insert-only, so no `&mut` to it can exist. + Some((unsafe { &*ptr }, weak)) + } + + pub fn alloc_slice_copy(&self, src: &[T]) -> Option<&[T]> { + let buf = self.alloc_scratch::(src.len())?; + for (slot, &value) in buf.iter_mut().zip(src) { + slot.write(value); + } + // SAFETY: every lane written above from `src`. + Some(unsafe { std::slice::from_raw_parts(buf.as_ptr().cast::(), src.len()) }) + } + + // Not drop-tracked: callers must either consume every written lane or + // restrict themselves to `Copy` payloads (leak, not UB, otherwise). + #[allow(clippy::mut_from_ref)] + pub fn alloc_scratch(&self, len: usize) -> Option<&mut [MaybeUninit]> { + let size = size_of::().checked_mul(len)?; + let offset = self.reserve(size, align_of::())?; + let ptr = unsafe { self.base().add(offset) }.cast::>(); + // SAFETY: exclusive region; lifetime tied to `&self`, and `reset` takes + // `&mut self`, so the slice cannot outlive the generation. + Some(unsafe { std::slice::from_raw_parts_mut(ptr, len) }) + } + + /// `false` once generations are exhausted, parking the arena on [`PARKED_GENERATION`] + /// where every handle misses and further allocation is refused. + pub fn reset(&mut self) -> bool { + let base = self.base(); + let entries = std::mem::take(self.drops.get_mut().unwrap()); + self.generation.store(PARKED_GENERATION, Ordering::Release); + for entry in entries.into_iter().rev() { + // SAFETY: registered at alloc time; insert-only means the region was + // never overwritten within this generation. + unsafe { (entry.drop_fn)(base.add(entry.offset)) } + } + *self.offset.get_mut() = 0; + let Some(generation) = next_generation() else { return false }; + self.generation.store(generation, Ordering::Release); + true + } +} + +impl Drop for Arena { + fn drop(&mut self) { + self.reset(); + LIVE_ARENAS.fetch_sub(1, Ordering::Release); + } +} + +pub struct ArenaWeak { + word: u64, + _marker: PhantomData<*const T>, +} + +impl Clone for ArenaWeak { + fn clone(&self) -> Self { + *self + } +} +impl Copy for ArenaWeak {} + +impl ArenaWeak { + pub const NULL: Self = ArenaWeak { word: 0, _marker: PhantomData }; + + /// `None` once either field leaves its encodable range, so an oversized or parked + /// arena refuses to hand out a handle rather than truncating it to a live address. + fn new(generation: u64, offset: usize) -> Option { + let offset = u64::try_from(offset).ok().filter(|offset| *offset <= OFFSET_MASK)?; + (generation <= GENERATION_MASK).then_some(Self { + word: (generation << OFFSET_BITS) | offset, + _marker: PhantomData, + }) + } + + pub fn upgrade(self, arena: &Arena) -> Option<&T> { + let generation = self.word >> OFFSET_BITS; + if generation != arena.generation() { + return None; + } + let offset = (self.word & OFFSET_MASK) as usize; + // SAFETY: same generation means the entry was fully written before its + // word was published (Release) and cannot move or be overwritten within + // a generation (insert-only); the Acquire load that produced this word + // ordered the payload writes. + Some(unsafe { &*arena.base().add(offset).cast::() }) + } +} + +pub struct ArenaCell { + word: AtomicU64, + _marker: PhantomData T>, +} + +impl Clone for ArenaCell { + fn clone(&self) -> Self { + Self { + word: AtomicU64::new(self.word.load(Ordering::Acquire)), + _marker: PhantomData, + } + } +} + +impl std::fmt::Debug for ArenaCell { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ArenaCell").field("word", &self.word.load(Ordering::Relaxed)).finish() + } +} + +impl Default for ArenaCell { + fn default() -> Self { + Self { + word: AtomicU64::new(0), + _marker: PhantomData, + } + } +} + +impl ArenaCell { + pub fn new() -> Self { + Self::default() + } + + pub fn load<'e>(&self, arena: &'e Arena) -> Option<&'e T> { + let weak = ArenaWeak:: { + word: self.word.load(Ordering::Acquire), + _marker: PhantomData, + }; + weak.upgrade(arena) + } + + pub fn store(&self, weak: ArenaWeak) { + self.word.store(weak.word, Ordering::Release); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::PoisonError; + use std::sync::atomic::AtomicU32; + + #[test] + fn alloc_upgrade_reset_miss() { + let _guard = COUNTER_GUARD.lock().unwrap_or_else(PoisonError::into_inner); + let mut arena = Arena::new(1024).unwrap(); + let cell = ArenaCell::new(); + let (value, weak) = arena.alloc(41u32).unwrap(); + assert_eq!(*value, 41); + cell.store(weak); + assert_eq!(cell.load(&arena), Some(&41)); + arena.reset(); + assert_eq!(cell.load(&arena), None, "stale handle must miss"); + } + + #[test] + fn capacity_survives_reset() { + let _guard = COUNTER_GUARD.lock().unwrap_or_else(PoisonError::into_inner); + let mut arena = Arena::new(64 + align_of::() - 1).unwrap(); + for _ in 0..10 { + for _ in 0..16 { + assert!(arena.alloc(0u32).is_some()); + } + assert!(arena.alloc(0u32).is_none(), "exhausted within generation"); + arena.reset(); + } + } + + #[test] + fn panics_leave_the_arena_coherent() { + let _guard = COUNTER_GUARD.lock().unwrap_or_else(PoisonError::into_inner); + fn assert_ref_unwind_safe() {} + assert_ref_unwind_safe::(); + + static DROPS: AtomicU32 = AtomicU32::new(0); + struct Probe(#[allow(dead_code)] String); + impl Drop for Probe { + fn drop(&mut self) { + DROPS.fetch_add(1, Ordering::Relaxed); + } + } + let mut arena = Arena::new(1024).unwrap(); + let cell = ArenaCell::new(); + let result = std::panic::catch_unwind(|| { + let (_, weak) = arena.alloc(Probe("pre-panic".into())).unwrap(); + cell.store(weak); + panic!("mid-eval"); + }); + assert!(result.is_err()); + assert!(cell.load(&arena).is_some(), "the generation is still live after the caught panic"); + arena.reset(); + assert_eq!(DROPS.load(Ordering::Relaxed), 1, "reset reclaims pre-panic allocations"); + assert!(cell.load(&arena).is_none(), "the bump kills stale handles"); + assert!(arena.alloc(0u32).is_some(), "the arena stays usable"); + } + + #[test] + fn a_panicking_destructor_leaves_no_resolvable_handle() { + let _guard = COUNTER_GUARD.lock().unwrap_or_else(PoisonError::into_inner); + struct Bomb; + impl Drop for Bomb { + fn drop(&mut self) { + panic!("payload destructor"); + } + } + let mut arena = Arena::new(1024).unwrap(); + let cell = ArenaCell::new(); + let (_, weak) = arena.alloc(Bomb).unwrap(); + cell.store(weak); + + let unwound = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| arena.reset())); + assert!(unwound.is_err(), "the panic must propagate out of reset"); + assert!(cell.load(&arena).is_none(), "a half-dropped generation must resolve no handle"); + } + + /// Held by every test that perturbs [`NEXT_GENERATION`], so a swapped-out counter + /// is never observed by a concurrently constructing test. + static COUNTER_GUARD: Mutex<()> = Mutex::new(()); + + #[test] + fn an_exhausted_reset_parks_the_arena_and_refuses_handles() { + let _guard = COUNTER_GUARD.lock().unwrap_or_else(PoisonError::into_inner); + let mut arena = Arena::new(1024).unwrap(); + let (_, weak) = arena.alloc(41u32).unwrap(); + + let restore = NEXT_GENERATION.swap(GENERATION_MASK + 1, Ordering::Relaxed); + assert!(!arena.reset(), "an exhausted counter must report failure"); + NEXT_GENERATION.store(restore, Ordering::Relaxed); + + assert_eq!(weak.upgrade(&arena), None, "a parked arena resolves no handle"); + assert_eq!(ArenaWeak::::NULL.upgrade(&arena), None, "not even the null handle"); + assert!(arena.alloc(0u32).is_none(), "a parked arena refuses allocation"); + } + + #[test] + fn the_generation_counter_rewinds_only_without_live_arenas() { + let _guard = COUNTER_GUARD.lock().unwrap_or_else(PoisonError::into_inner); + let arena = Arena::new(64).unwrap(); + assert!(!unsafe { reset_generation_counter() }, "a live arena must block the rewind"); + drop(arena); + } + + #[test] + fn handles_do_not_upgrade_against_a_foreign_arena() { + let _guard = COUNTER_GUARD.lock().unwrap_or_else(PoisonError::into_inner); + let first = Arena::new(1024).unwrap(); + let second = Arena::new(1024).unwrap(); + let (_, weak) = first.alloc(41u32).unwrap(); + assert_eq!(weak.upgrade(&first), Some(&41)); + assert_eq!(weak.upgrade(&second), None, "a handle must not resolve against another arena"); + assert_eq!(ArenaWeak::::NULL.upgrade(&second), None, "the null handle never upgrades"); + } + + #[test] + fn reset_drops_dependents_before_their_dependencies() { + let _guard = COUNTER_GUARD.lock().unwrap_or_else(PoisonError::into_inner); + static ORDER: Mutex> = Mutex::new(Vec::new()); + struct Probe(u32); + impl Drop for Probe { + fn drop(&mut self) { + ORDER.lock().unwrap().push(self.0); + } + } + let mut arena = Arena::new(1024).unwrap(); + for id in 0..3 { + arena.alloc(Probe(id)).unwrap(); + } + arena.reset(); + assert_eq!(*ORDER.lock().unwrap(), vec![2, 1, 0], "later allocations may borrow earlier ones, so they drop first"); + } + + #[test] + fn drop_glue_runs_on_reset() { + let _guard = COUNTER_GUARD.lock().unwrap_or_else(PoisonError::into_inner); + static DROPS: AtomicU32 = AtomicU32::new(0); + struct Probe(#[allow(dead_code)] String); + impl Drop for Probe { + fn drop(&mut self) { + DROPS.fetch_add(1, Ordering::Relaxed); + } + } + let mut arena = Arena::new(1024).unwrap(); + arena.alloc(Probe("owns heap".into())).unwrap(); + arena.alloc(Probe("me too".into())).unwrap(); + assert_eq!(DROPS.load(Ordering::Relaxed), 0); + arena.reset(); + assert_eq!(DROPS.load(Ordering::Relaxed), 2); + } +} diff --git a/node-graph/libraries/core-types/src/context.rs b/node-graph/libraries/core-types/src/context.rs index ab8b022239..b122f673d1 100644 --- a/node-graph/libraries/core-types/src/context.rs +++ b/node-graph/libraries/core-types/src/context.rs @@ -1,8 +1,8 @@ +use crate::arena::Arena; 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::hash::{Hash, Hasher}; use std::panic::Location; use std::sync::Arc; @@ -36,6 +36,9 @@ pub trait ExtractPosition { } pub trait ExtractIndex { fn try_index(&self) -> Option>; + fn innermost_index(&self) -> u64 { + self.try_index().and_then(|mut indices| indices.next()).unwrap_or(0) as u64 + } } pub trait ExtractVarArgs { // TODO: Consider returning a slice or something like that @@ -62,7 +65,9 @@ pub trait InjectRealTime {} pub trait InjectAnimationTime {} pub trait InjectPointerPosition {} pub trait InjectPosition {} -pub trait InjectIndex {} +pub trait InjectIndex { + fn set_index(&mut self, index: u64); +} pub trait InjectVarArgs {} // ================ @@ -101,7 +106,6 @@ impl InjectRealTime for T {} impl InjectAnimationTime for T {} impl InjectPointerPosition for T {} impl InjectPosition for T {} -impl InjectIndex for T {} impl InjectVarArgs for T {} // ============= @@ -190,11 +194,120 @@ impl ContextFeatures { // CONTEXT DEPENDENCIES // ==================== -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, graphene_hash::CacheHash, dyn_any::DynAny, Default)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, graphene_hash::CacheHash, dyn_any::DynAny, Default)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct ContextDependencies { pub extract: ContextFeatures, pub inject: ContextFeatures, + #[cfg_attr(feature = "serde", serde(default, deserialize_with = "deserialize_sorted_sources"))] + sources: Vec, +} + +impl ContextDependencies { + pub fn new(extract: ContextFeatures, inject: ContextFeatures) -> Self { + Self { extract, inject, sources: Vec::new() } + } + + pub fn sources(&self) -> &[SourceId] { + &self.sources + } + + pub fn add_sources(&mut self, sources: &[SourceId]) { + merge_sorted_sources(&mut self.sources, sources); + } + + pub fn from_sources(sources: &[SourceId]) -> Self { + let mut dependencies = Self::default(); + dependencies.add_sources(sources); + dependencies + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, graphene_hash::CacheHash, dyn_any::DynAny, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct ContextModification { + pub features: ContextFeatures, + #[cfg_attr(feature = "serde", serde(deserialize_with = "deserialize_sorted_sources"))] + sources: Vec, +} + +impl ContextModification { + pub fn sources(&self) -> &[SourceId] { + &self.sources + } + + pub fn add_sources(&mut self, sources: &[SourceId]) { + merge_sorted_sources(&mut self.sources, sources); + } + + pub fn from_sources(features: ContextFeatures, sources: &[SourceId]) -> Self { + let mut modification = Self { features, sources: Vec::new() }; + modification.add_sources(sources); + modification + } +} + +/// Restores the sorted-and-deduplicated invariant that `contains` and `difference` +/// rely on for binary search, which arbitrary serialized input can violate. +#[cfg(feature = "serde")] +fn deserialize_sorted_sources<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result, D::Error> { + use serde::Deserialize; + let mut sources = Vec::deserialize(deserializer)?; + sources.sort_unstable(); + sources.dedup(); + Ok(sources) +} + +impl core::ops::BitOrAssign<&ContextModification> for ContextModification { + fn bitor_assign(&mut self, other: &Self) { + self.features |= other.features; + merge_sorted_sources(&mut self.sources, &other.sources); + } +} + +impl core::ops::BitOrAssign for ContextModification { + fn bitor_assign(&mut self, other: Self) { + *self |= &other; + } +} + +impl core::ops::BitOrAssign for ContextModification { + fn bitor_assign(&mut self, features: ContextFeatures) { + self.features |= features; + } +} + +impl core::ops::BitAndAssign for ContextModification { + fn bitand_assign(&mut self, features: ContextFeatures) { + self.features &= features; + } +} + +impl ContextModification { + pub fn contains(&self, other: &Self) -> bool { + debug_assert!(self.sources.is_sorted() && other.sources.is_sorted()); + self.features.contains(other.features) && other.sources.iter().all(|id| self.sources.binary_search(id).is_ok()) + } + + pub fn difference(&self, other: &Self) -> Self { + debug_assert!(other.sources.is_sorted()); + Self { + features: self.features.difference(other.features), + sources: self.sources.iter().copied().filter(|id| other.sources.binary_search(id).is_err()).collect(), + } + } + + pub fn is_empty(&self) -> bool { + self.features.is_empty() && self.sources.is_empty() + } +} + +/// Sorts and deduplicates unconditionally, so the result is ordered no matter what +/// order the inputs arrived in. +pub fn merge_sorted_sources(sources: &mut Vec, other: &[SourceId]) { + sources.extend_from_slice(other); + sources.sort_unstable(); + sources.dedup(); } impl From<&[ContextFeature]> for ContextDependencies { @@ -223,7 +336,7 @@ impl From<&[ContextFeature]> for ContextDependencies { _ => ContextFeatures::empty(), }; } - Self { extract, inject } + Self { extract, inject, sources: Vec::new() } } } @@ -383,324 +496,653 @@ impl ExtractFootprint for () { } } -// ===================================== -// EXTRACT TRAIT IMPLS FOR `ContextImpl` -// ===================================== +// ============== +// TYPE `Context` +// ============== + +pub type Context<'a> = ContextImpl<'a>; +type DynRef<'a> = &'a (dyn Any + Send + Sync); -impl Ctx for ContextImpl<'_> {} +pub trait DynHash { + fn dyn_hash(&self, state: &mut dyn Hasher); +} -impl ExtractFootprint for ContextImpl<'_> { - fn try_footprint(&self) -> Option<&Footprint> { - self.footprint +impl DynHash for H { + fn dyn_hash(&self, mut state: &mut dyn Hasher) { + graphene_hash::CacheHash::cache_hash(self, &mut state); } } -impl ExtractRealTime for ContextImpl<'_> { - fn try_real_time(&self) -> Option { - self.real_time + +impl Hash for dyn AnyHash { + fn hash(&self, state: &mut H) { + self.dyn_hash(state); } } -impl ExtractPosition for ContextImpl<'_> { - fn try_position(&self) -> Option> { - self.position.clone().map(|x| x.into_iter()) +impl Hash for Box { + fn hash(&self, state: &mut H) { + (**self).dyn_hash(state); } } -impl ExtractIndex for ContextImpl<'_> { - fn try_index(&self) -> Option> { - self.index.clone().map(|x| x.into_iter()) + +pub trait AnyHash: DynHash + Any {} +impl AnyHash for T {} + +pub trait VarArg: AnyHash { + fn clone_slot(&self) -> OwnedSlot; +} + +impl VarArg for T { + fn clone_slot(&self) -> OwnedSlot { + OwnedSlot(Box::new(self.clone())) } } -impl ExtractVarArgs for ContextImpl<'_> { - fn vararg(&self, index: usize) -> Result, VarArgsResult> { - let Some(inner) = self.varargs else { return Err(VarArgsResult::NoVarArgs) }; - inner.get(index).ok_or(VarArgsResult::IndexOutOfBounds).copied() + +pub struct OwnedSlot(Box); + +impl Clone for OwnedSlot { + fn clone(&self) -> Self { + self.0.clone_slot() } +} - fn varargs_len(&self) -> Result { - let Some(inner) = self.varargs else { return Err(VarArgsResult::NoVarArgs) }; - Ok(inner.len()) +impl std::ops::Deref for OwnedSlot { + type Target = dyn VarArg + Send + Sync; + + fn deref(&self) -> &Self::Target { + self.0.as_ref() + } +} + +impl std::fmt::Debug for OwnedSlot { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("OwnedSlot") + } +} + +pub type SourceId = u64; + +#[derive(Clone, Copy, Debug)] +pub struct IndexLink<'a> { + pub index: u64, + pub outer: Option<&'a IndexLink<'a>>, +} + +#[derive(Clone, Copy, Debug)] +pub struct PositionLink<'a> { + pub position: DVec2, + pub outer: Option<&'a PositionLink<'a>>, +} + +pub type DynSlot<'a> = &'a (dyn VarArg + Send + Sync); + +#[derive(Clone, Copy)] +pub enum VarArgSlots<'a> { + Single(DynSlot<'a>), + Slice(&'a [DynSlot<'a>]), +} + +impl<'a> VarArgSlots<'a> { + pub fn get(&self, index: usize) -> Option> { + match self { + VarArgSlots::Single(slot) => (index == 0).then_some(*slot), + VarArgSlots::Slice(slots) => slots.get(index).copied(), + } + } + + pub fn len(&self) -> usize { + match self { + VarArgSlots::Single(_) => 1, + VarArgSlots::Slice(slots) => slots.len(), + } + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + pub fn iter(&self) -> impl Iterator> + '_ { + (0..self.len()).filter_map(move |index| self.get(index)) + } +} + +#[derive(Clone, Copy)] +pub struct VarArgLink<'a> { + pub args: VarArgSlots<'a>, + pub outer: Option<&'a VarArgLink<'a>>, +} + +impl<'a> std::fmt::Debug for VarArgLink<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("VarArgLink").field("args_len", &self.args.len()).field("outer", &self.outer).finish() + } +} + +#[derive(Clone, Copy, Debug)] +pub struct EvalScope<'a> { + real_time: Option, + animation_time: Option, + pointer_position: Option, + generations: &'a [(SourceId, u64)], + arena: &'a Arena, + hash: u64, +} + +impl<'a> EvalScope<'a> { + pub fn new(real_time: Option, animation_time: Option, pointer_position: Option, generations: &'a [(SourceId, u64)], arena: &'a Arena) -> Self { + let mut scope = Self { + real_time, + animation_time, + pointer_position, + generations, + arena, + hash: 0, + }; + scope.hash = scope.compute_hash(|_| true); + scope + } + + pub fn with_real_time(&self, real_time: Option) -> EvalScope<'a> { + let mut scope = EvalScope { real_time, ..*self }; + scope.hash = scope.compute_hash(|_| true); + scope + } + + pub fn with_animation_time(&self, animation_time: Option) -> EvalScope<'a> { + let mut scope = EvalScope { animation_time, ..*self }; + scope.hash = scope.compute_hash(|_| true); + scope + } + + pub fn with_pointer_position(&self, pointer_position: Option) -> EvalScope<'a> { + let mut scope = EvalScope { pointer_position, ..*self }; + scope.hash = scope.compute_hash(|_| true); + scope + } + + pub fn nullified(&self, keep: ContextFeatures, retain: Option<&[SourceId]>) -> EvalScope<'a> { + let mut scope = EvalScope { + real_time: self.real_time.filter(|_| keep.contains(ContextFeatures::REAL_TIME)), + animation_time: self.animation_time.filter(|_| keep.contains(ContextFeatures::ANIMATION_TIME)), + pointer_position: self.pointer_position.filter(|_| keep.contains(ContextFeatures::POINTER_POSITION)), + ..*self + }; + scope.hash = scope.compute_hash(|source| retain.is_none_or(|retain| retain.contains(source))); + scope + } + + pub fn excluding(&self, source: SourceId) -> EvalScope<'a> { + let mut scope = *self; + scope.hash = scope.compute_hash(|candidate| *candidate != source); + scope + } + + fn compute_hash(&self, keep_source: impl Fn(&SourceId) -> bool) -> u64 { + let mut hasher = std::hash::DefaultHasher::new(); + self.real_time.map(f64::to_bits).hash(&mut hasher); + self.animation_time.map(f64::to_bits).hash(&mut hasher); + self.pointer_position.map(|position| (position.x.to_bits(), position.y.to_bits())).hash(&mut hasher); + for (source, generation) in self.generations { + if keep_source(source) { + (source, generation).hash(&mut hasher); + } + } + hasher.finish() + } + + pub fn generation(&self, source: SourceId) -> Option { + self.generations.iter().find(|(candidate, _)| *candidate == source).map(|(_, generation)| *generation) + } + + pub fn generations(&self) -> &'a [(SourceId, u64)] { + self.generations } - fn hash_varargs(&self, _hasher: &mut dyn Hasher) { - todo!() + pub fn arena(&self) -> &'a Arena { + self.arena } } -// ========================================== -// EXTRACT TRAIT IMPLS FOR `OwnedContextImpl` -// ========================================== +pub trait ExtractArena { + type ArenaRef; + fn arena(&self) -> Self::ArenaRef; +} + +pub trait CtxFamily { + type Ctx<'s>: Ctx + DeriveCtx; +} + +pub type Derived<'s, C> = <::Family as CtxFamily>::Ctx<'s>; + +pub trait DeriveCtx { + type Family: CtxFamily; + fn derived(&self) -> Derived<'_, Self>; + fn index_head(&self) -> IndexLink<'_>; + fn scope(&self) -> &EvalScope<'_>; + fn position_head(&self) -> Option<&PositionLink<'_>>; + fn varargs_head(&self) -> Option<&VarArgLink<'_>>; + fn promoted<'s>(&'s self, spilled_head: &'s IndexLink<'s>, inner_index: u64) -> Derived<'s, Self>; + fn with_footprint<'s>(&'s self, footprint: &'s Footprint) -> Derived<'s, Self>; + fn with_varargs<'s>(&'s self, varargs: &'s VarArgLink<'s>) -> Derived<'s, Self>; + fn with_position<'s>(&'s self, position: &'s PositionLink<'s>) -> Derived<'s, Self>; + fn with_scope<'s>(&'s self, scope: &'s EvalScope<'s>) -> Derived<'s, Self>; + fn nullified<'s>(&'s self, keep: ContextFeatures, scope: &'s EvalScope<'s>) -> Derived<'s, Self>; + + fn modify_footprint(&self, modify: impl FnOnce(&mut Footprint)) -> ModifiedFootprint<'_, Self> + where + Self: ExtractFootprint + Sized, + { + let mut footprint = self.try_footprint().copied(); + if let Some(footprint) = &mut footprint { + modify(footprint); + } + ModifiedFootprint { ctx: self, footprint } + } + + fn push_vararg<'s>(&'s self, arg: DynSlot<'s>) -> VarArgScope<'s, Self> + where + Self: Sized, + { + VarArgScope { + ctx: self, + link: VarArgLink { + args: VarArgSlots::Single(arg), + outer: self.varargs_head(), + }, + } + } -impl ArcCtx for OwnedContextImpl {} + fn push_position(&self, position: DVec2) -> PositionScope<'_, Self> + where + Self: Sized, + { + PositionScope { + ctx: self, + link: PositionLink { + position, + outer: self.position_head(), + }, + } + } +} -impl ExtractFootprint for OwnedContextImpl { +pub struct PositionScope<'c, C> { + ctx: &'c C, + link: PositionLink<'c>, +} + +impl PositionScope<'_, C> { + pub fn ctx(&self) -> Derived<'_, C> { + self.ctx.with_position(&self.link) + } +} + +#[derive(Clone, Debug, Default)] +pub struct CtxSnapshot { + footprint: Option, + real_time: Option, + animation_time: Option, + pointer_position: Option, + index: Option>, + positions: Option>, + varargs: Vec>, + generations: Vec<(SourceId, u64)>, +} + +impl CtxSnapshot { + pub fn capture(ctx: &C) -> Self + where + C: DeriveCtx + ExtractFootprint + ExtractRealTime + ExtractAnimationTime + ExtractPointerPosition + ExtractIndex + ExtractPosition, + { + Self { + footprint: ctx.try_footprint().copied(), + real_time: ctx.try_real_time(), + animation_time: ctx.try_animation_time(), + pointer_position: ctx.try_pointer_position(), + index: ctx.try_index().map(|levels| levels.collect()), + positions: ctx.try_position().map(|positions| positions.collect()), + varargs: std::iter::successors(ctx.varargs_head(), |link| link.outer) + .map(|link| link.args.iter().map(|slot| slot.clone_slot()).collect()) + .collect(), + generations: ctx.scope().generations().to_vec(), + } + } + + pub fn generations(&self) -> &[(SourceId, u64)] { + &self.generations + } +} + +impl ExtractFootprint for CtxSnapshot { fn try_footprint(&self) -> Option<&Footprint> { self.footprint.as_ref() } } -impl ExtractRealTime for OwnedContextImpl { + +impl ExtractRealTime for CtxSnapshot { fn try_real_time(&self) -> Option { self.real_time } } -impl ExtractAnimationTime for OwnedContextImpl { + +impl ExtractAnimationTime for CtxSnapshot { fn try_animation_time(&self) -> Option { self.animation_time } } -impl ExtractPointerPosition for OwnedContextImpl { + +impl ExtractPointerPosition for CtxSnapshot { fn try_pointer_position(&self) -> Option { self.pointer_position } } -impl ExtractPosition for OwnedContextImpl { - fn try_position(&self) -> Option> { - self.position.clone().map(|x| x.into_iter()) + +impl ExtractIndex for CtxSnapshot { + fn try_index(&self) -> Option> { + self.index.as_ref().map(|levels| levels.iter().copied()) } } -impl ExtractIndex for OwnedContextImpl { - fn try_index(&self) -> Option> { - self.index.clone().map(|x| x.into_iter()) + +impl ExtractPosition for CtxSnapshot { + fn try_position(&self) -> Option> { + self.positions.as_ref().map(|positions| positions.iter().copied()) } } -impl ExtractVarArgs for OwnedContextImpl { + +impl ExtractVarArgs for CtxSnapshot { fn vararg(&self, index: usize) -> Result, VarArgsResult> { - let Some(ref inner) = self.varargs else { - let Some(ref parent) = self.parent else { - return Err(VarArgsResult::NoVarArgs); - }; - return parent.vararg(index); - }; - inner.get(index).map(|x| x.as_ref() as DynRef<'_>).ok_or(VarArgsResult::IndexOutOfBounds) + if self.varargs.is_empty() { + return Err(VarArgsResult::NoVarArgs); + } + let slot = self.varargs.iter().flatten().nth(index).ok_or(VarArgsResult::IndexOutOfBounds)?; + Ok(&**slot as DynRef<'_>) } fn varargs_len(&self) -> Result { - let Some(ref inner) = self.varargs else { - let Some(ref parent) = self.parent else { - return Err(VarArgsResult::NoVarArgs); - }; - return parent.varargs_len(); - }; - Ok(inner.len()) + if self.varargs.is_empty() { + return Err(VarArgsResult::NoVarArgs); + } + Ok(self.varargs.iter().map(|level| level.len()).sum()) } - fn hash_varargs(&self, mut hasher: &mut dyn Hasher) { - match (&self.varargs, &self.parent) { - (Some(inner), _) => { - for arg in inner.iter() { - arg.hash(&mut hasher); - } - } - (None, Some(parent)) => { - parent.hash_varargs(hasher); - } - _ => (), - }; + fn hash_varargs(&self, hasher: &mut dyn Hasher) { + let mut count = 0u64; + for slot in self.varargs.iter().flatten() { + slot.dyn_hash(&mut *hasher); + count += 1; + } + count.hash(&mut &mut *hasher); } } -impl CloneVarArgs for Arc { - fn arc_clone(&self) -> Option> { - Some(self.clone()) - } +pub struct VarArgScope<'c, C> { + ctx: &'c C, + link: VarArgLink<'c>, } -// ====================================== -// TYPES `Context` AND `OwnedContextImpl` -// ====================================== - -pub type Context<'a> = Option>; -type DynRef<'a> = &'a (dyn Any + Send + Sync); -type DynBox = Box; +impl VarArgScope<'_, C> { + pub fn ctx(&self) -> Derived<'_, C> { + self.ctx.with_varargs(&self.link) + } +} -#[derive(dyn_any::DynAny)] -pub struct OwnedContextImpl { - parent: Option>, +pub struct ModifiedFootprint<'c, C> { + ctx: &'c C, footprint: Option, - real_time: Option, - animation_time: Option, - pointer_position: Option, - position: Option>, - // This could be converted into a single enum to save extra bytes - index: Option>, - varargs: Option>, } -impl std::fmt::Debug for OwnedContextImpl { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("OwnedContextImpl") - .field("parent", &self.parent.as_ref().map(|_| "")) - .field("footprint", &self.footprint) - .field("real_time", &self.real_time) - .field("animation_time", &self.animation_time) - .field("pointer_position", &self.pointer_position) - .field("index", &self.index) - .field("varargs_len", &self.varargs.as_ref().map(|x| x.len())) - .finish() +impl ModifiedFootprint<'_, C> { + pub fn ctx(&self) -> Derived<'_, C> { + match &self.footprint { + Some(footprint) => self.ctx.with_footprint(footprint), + None => self.ctx.derived(), + } } } -impl Default for OwnedContextImpl { - #[track_caller] - fn default() -> Self { - Self::empty() - } +pub struct ContextImplFamily; + +impl CtxFamily for ContextImplFamily { + type Ctx<'s> = ContextImpl<'s>; } -impl graphene_hash::CacheHash for OwnedContextImpl { - fn cache_hash(&self, state: &mut H) { - self.footprint.cache_hash(state); - self.real_time.cache_hash(state); - self.animation_time.cache_hash(state); - self.pointer_position.cache_hash(state); - self.position.cache_hash(state); - self.index.cache_hash(state); - self.hash_varargs(state); - } +#[derive(Clone, Copy, Debug)] +pub struct ContextImpl<'a> { + index: IndexLink<'a>, + position: Option<&'a PositionLink<'a>>, + varargs: Option<&'a VarArgLink<'a>>, + footprint: Option<&'a Footprint>, + scope: &'a EvalScope<'a>, } -impl OwnedContextImpl { - #[track_caller] - pub fn from(value: T) -> Self { - OwnedContextImpl::from_flags(value, ContextFeatures::all()) +impl<'a> ContextImpl<'a> { + pub fn root(scope: &'a EvalScope<'a>) -> Self { + Self { + index: IndexLink { index: 0, outer: None }, + position: None, + varargs: None, + footprint: None, + scope, + } } - #[track_caller] - pub fn from_flags(value: T, bitflags: ContextFeatures) -> Self { - let parent = bitflags - .contains(ContextFeatures::VARARGS) - .then(|| match value.varargs_len() { - Ok(x) if x > 0 => value.arc_clone(), - _ => None, - }) - .flatten(); - let footprint = bitflags.contains(ContextFeatures::FOOTPRINT).then(|| value.try_footprint().copied()).flatten(); - let real_time = bitflags.contains(ContextFeatures::REAL_TIME).then(|| value.try_real_time()).flatten(); - let animation_time = bitflags.contains(ContextFeatures::ANIMATION_TIME).then(|| value.try_animation_time()).flatten(); - let pointer_position = bitflags.contains(ContextFeatures::POINTER_POSITION).then(|| value.try_pointer_position()).flatten(); - let position = bitflags.contains(ContextFeatures::POSITION).then(|| value.try_position()).flatten().map(|x| x.collect()); - let index = bitflags.contains(ContextFeatures::INDEX).then(|| value.try_index()).flatten().map(|x| x.collect()); - - OwnedContextImpl { - parent, - footprint, - real_time, - animation_time, - pointer_position, - position, - index, - varargs: None, + pub fn scope(&self) -> &'a EvalScope<'a> { + self.scope + } + + pub fn index_head(&self) -> IndexLink<'a> { + self.index + } + + pub fn with_footprint<'s>(&self, footprint: &'s Footprint) -> ContextImpl<'s> + where + 'a: 's, + { + ContextImpl { footprint: Some(footprint), ..*self } + } + + pub fn with_scope<'s>(&self, scope: &'s EvalScope<'s>) -> ContextImpl<'s> + where + 'a: 's, + { + ContextImpl { scope, ..*self } + } + + pub fn with_varargs<'s>(&self, varargs: &'s VarArgLink<'s>) -> ContextImpl<'s> + where + 'a: 's, + { + ContextImpl { varargs: Some(varargs), ..*self } + } + + pub fn with_position<'s>(&self, position: &'s PositionLink<'s>) -> ContextImpl<'s> + where + 'a: 's, + { + ContextImpl { position: Some(position), ..*self } + } + + pub fn nullified<'s>(&self, keep: ContextFeatures, scope: &'s EvalScope<'s>) -> ContextImpl<'s> + where + 'a: 's, + { + ContextImpl { + index: match keep.contains(ContextFeatures::INDEX) { + true => self.index, + false => IndexLink { index: 0, outer: None }, + }, + position: self.position.filter(|_| keep.contains(ContextFeatures::POSITION)), + varargs: self.varargs.filter(|_| keep.contains(ContextFeatures::VARARGS)), + footprint: self.footprint.filter(|_| keep.contains(ContextFeatures::FOOTPRINT)), + scope, } } - pub const fn empty() -> Self { - OwnedContextImpl { - parent: None, - footprint: None, - real_time: None, - animation_time: None, - pointer_position: None, - position: None, - index: None, - varargs: None, + pub fn promoted<'s>(&self, spilled_head: &'s IndexLink<'s>, inner_index: u64) -> ContextImpl<'s> + where + 'a: 's, + { + ContextImpl { + index: IndexLink { + index: inner_index, + outer: Some(spilled_head), + }, + ..*self } } } -pub trait DynHash { - fn dyn_hash(&self, state: &mut dyn Hasher); +impl Ctx for ContextImpl<'_> {} + +impl InjectIndex for ContextImpl<'_> { + fn set_index(&mut self, index: u64) { + self.index.index = index; + } } -impl DynHash for H { - fn dyn_hash(&self, mut state: &mut dyn Hasher) { - graphene_hash::CacheHash::cache_hash(self, &mut state); +impl ExtractFootprint for ContextImpl<'_> { + fn try_footprint(&self) -> Option<&Footprint> { + self.footprint + } +} +impl ExtractRealTime for ContextImpl<'_> { + fn try_real_time(&self) -> Option { + self.scope.real_time } } +impl ExtractAnimationTime for ContextImpl<'_> { + fn try_animation_time(&self) -> Option { + self.scope.animation_time + } +} +impl ExtractPointerPosition for ContextImpl<'_> { + fn try_pointer_position(&self) -> Option { + self.scope.pointer_position + } +} +impl ExtractIndex for ContextImpl<'_> { + fn try_index(&self) -> Option> { + Some(std::iter::successors(Some(&self.index), |link| link.outer).map(|link| link.index as usize)) + } +} +impl ExtractPosition for ContextImpl<'_> { + fn try_position(&self) -> Option> { + self.position.map(|head| std::iter::successors(Some(head), |link| link.outer).map(|link| link.position)) + } +} +impl ExtractVarArgs for ContextImpl<'_> { + fn vararg(&self, index: usize) -> Result, VarArgsResult> { + let mut link = self.varargs.ok_or(VarArgsResult::NoVarArgs)?; + let mut remaining = index; + loop { + match link.args.get(remaining) { + Some(arg) => return Ok(arg as DynRef<'_>), + None => { + remaining -= link.args.len(); + link = link.outer.ok_or(VarArgsResult::IndexOutOfBounds)?; + } + } + } + } -impl Hash for dyn AnyHash { - fn hash(&self, state: &mut H) { - self.dyn_hash(state); + fn varargs_len(&self) -> Result { + let head = self.varargs.ok_or(VarArgsResult::NoVarArgs)?; + Ok(std::iter::successors(Some(head), |link| link.outer).map(|link| link.args.len()).sum()) + } + + fn hash_varargs(&self, hasher: &mut dyn Hasher) { + let mut count = 0u64; + let mut link = self.varargs; + while let Some(current) = link { + for arg in current.args.iter() { + arg.dyn_hash(&mut *hasher); + count += 1; + } + link = current.outer; + } + count.hash(&mut &mut *hasher); } } -impl Hash for Box { - fn hash(&self, state: &mut H) { - (**self).dyn_hash(state); +impl<'a> ExtractArena for ContextImpl<'a> { + type ArenaRef = &'a Arena; + fn arena(&self) -> &'a Arena { + self.scope.arena } } -pub trait AnyHash: DynHash + Any {} -impl AnyHash for T {} +impl<'a> DeriveCtx for ContextImpl<'a> { + type Family = ContextImplFamily; -impl OwnedContextImpl { - pub fn set_footprint(&mut self, footprint: Footprint) { - self.footprint = Some(footprint); + fn derived(&self) -> ContextImpl<'_> { + *self } - pub fn with_footprint(mut self, footprint: Footprint) -> Self { - self.footprint = Some(footprint); - self + fn index_head(&self) -> IndexLink<'_> { + self.index } - pub fn with_real_time(mut self, real_time: f64) -> Self { - self.real_time = Some(real_time); - self + + fn scope(&self) -> &EvalScope<'_> { + self.scope } - pub fn with_animation_time(mut self, animation_time: f64) -> Self { - self.animation_time = Some(animation_time); - self + + fn position_head(&self) -> Option<&PositionLink<'_>> { + self.position } - pub fn with_pointer_position(mut self, pointer_position: DVec2) -> Self { - self.pointer_position = Some(pointer_position); - self + + fn varargs_head(&self) -> Option<&VarArgLink<'_>> { + self.varargs } - pub fn with_position(mut self, position: DVec2) -> Self { - if let Some(current_position) = &mut self.position { - current_position.insert(0, position); - } else { - self.position = Some(vec![position]); - } - self + + fn promoted<'s>(&'s self, spilled_head: &'s IndexLink<'s>, inner_index: u64) -> ContextImpl<'s> { + ContextImpl::promoted(self, spilled_head, inner_index) } - pub fn with_index(mut self, index: usize) -> Self { - if let Some(current_index) = &mut self.index { - current_index.insert(0, index); - } else { - self.index = Some(vec![index]); - } - self + + fn with_footprint<'s>(&'s self, footprint: &'s Footprint) -> ContextImpl<'s> { + ContextImpl::with_footprint(self, footprint) } - pub fn with_vararg(mut self, value: Box) -> Self { - assert!(self.varargs.is_none_or(|value| value.is_empty())); - self.varargs = Some(Arc::new([value])); - self + + fn with_varargs<'s>(&'s self, varargs: &'s VarArgLink<'s>) -> ContextImpl<'s> { + ContextImpl::with_varargs(self, varargs) } - pub fn into_context(self) -> Option> { - Some(Arc::new(self)) + + fn with_position<'s>(&'s self, position: &'s PositionLink<'s>) -> ContextImpl<'s> { + ContextImpl::with_position(self, position) } - pub fn erase_parent(mut self) -> Self { - self.parent = None; - self + + fn with_scope<'s>(&'s self, scope: &'s EvalScope<'s>) -> ContextImpl<'s> { + ContextImpl::with_scope(self, scope) } -} -#[derive(Default, Clone, dyn_any::DynAny)] -pub struct ContextImpl<'a> { - pub(crate) footprint: Option<&'a Footprint>, - real_time: Option, - position: Option>, // This could be converted into a single enum to save extra bytes - index: Option>, // This could be converted into a single enum to save extra bytes - varargs: Option<&'a [DynRef<'a>]>, + fn nullified<'s>(&'s self, keep: ContextFeatures, scope: &'s EvalScope<'s>) -> ContextImpl<'s> { + ContextImpl::nullified(self, keep, scope) + } } -impl<'a> ContextImpl<'a> { - pub fn with_footprint<'f>(&self, new_footprint: &'f Footprint, varargs: Option<&'f impl Borrow<[DynRef<'f>]>>) -> ContextImpl<'f> - where - 'a: 'f, - { - ContextImpl { - footprint: Some(new_footprint), - position: self.position.clone(), - index: self.index.clone(), - varargs: varargs.map(|x| x.borrow()), - ..*self +impl graphene_hash::CacheHash for ContextImpl<'_> { + fn cache_hash(&self, state: &mut H) { + match self.footprint { + Some(footprint) => { + 1u8.hash(state); + footprint.cache_hash(state); + } + None => 0u8.hash(state), + } + let mut count = 0u64; + for link in std::iter::successors(Some(&self.index), |link| link.outer) { + link.index.hash(state); + count += 1; + } + count.hash(state); + count = 0; + let mut position = self.position; + while let Some(link) = position { + link.position.x.to_bits().hash(state); + link.position.y.to_bits().hash(state); + count += 1; + position = link.outer; } + count.hash(state); + self.hash_varargs(state); + self.scope.hash.hash(state); } } @@ -709,3 +1151,257 @@ pub enum VarArgsResult { IndexOutOfBounds, NoVarArgs, } + +#[cfg(test)] +mod context_impl_tests { + use super::*; + use crate::graphene_hash::CacheHash; + + fn hash_of(ctx: &ContextImpl) -> u64 { + let mut hasher = std::hash::DefaultHasher::new(); + ctx.cache_hash(&mut hasher); + hasher.finish() + } + + fn scope_fixture<'a>(generations: &'a [(SourceId, u64)], arena: &'a Arena) -> EvalScope<'a> { + EvalScope::new(Some(0.5), Some(1.5), None, generations, arena) + } + + #[test] + fn equal_contexts_hash_equal() { + let arena = Arena::new(64).unwrap(); + let generations = [(0, 1), (1, 3)]; + let scope = scope_fixture(&generations, &arena); + let a = ContextImpl::root(&scope); + let b = ContextImpl::root(&scope); + assert_eq!(hash_of(&a), hash_of(&b)); + } + + #[test] + fn each_axis_changes_the_hash() { + let arena = Arena::new(64).unwrap(); + let generations = [(0, 1)]; + let scope = scope_fixture(&generations, &arena); + let root = ContextImpl::root(&scope); + let footprint = Footprint::DEFAULT; + + let mut indexed = root; + indexed.set_index(4); + let position_link = PositionLink { + position: DVec2::new(1.0, 2.0), + outer: None, + }; + let time_scope = scope.with_real_time(Some(9.75)); + let variants = [root.with_footprint(&footprint), indexed, root.with_position(&position_link), root.with_scope(&time_scope)]; + let root_hash = hash_of(&root); + let mut hashes: Vec = variants.iter().map(hash_of).collect(); + hashes.push(root_hash); + hashes.sort(); + hashes.dedup(); + assert_eq!(hashes.len(), variants.len() + 1, "every axis must contribute to the hash"); + } + + #[test] + fn index_level_order_matters() { + let arena = Arena::new(64).unwrap(); + let generations = []; + let scope = scope_fixture(&generations, &arena); + let root = ContextImpl::root(&scope); + + let outer_one = IndexLink { index: 1, outer: None }; + let mut one_two = root.promoted(&outer_one, 2); + let outer_two = IndexLink { index: 2, outer: None }; + let mut two_one = root.promoted(&outer_two, 1); + assert_ne!(hash_of(&one_two), hash_of(&two_one)); + + one_two.set_index(7); + two_one.set_index(7); + assert_ne!(hash_of(&one_two), hash_of(&two_one), "outer levels must stay hashed after set_index"); + } + + #[test] + fn axis_boundaries_are_unambiguous() { + let arena = Arena::new(64).unwrap(); + let generations = []; + let scope = scope_fixture(&generations, &arena); + let root = ContextImpl::root(&scope); + + let position = PositionLink { + position: DVec2::new(5.0, 5.0), + outer: None, + }; + let spilled = root.index_head(); + let mut deep_index = root.promoted(&spilled, 0); + deep_index.set_index(5); + let shallow_with_position = root.with_position(&position); + assert_ne!(hash_of(&deep_index), hash_of(&shallow_with_position), "index levels must not blur into position levels"); + } + + #[test] + fn retain_scopes_generation_invalidation() { + let arena = Arena::new(64).unwrap(); + let initial = [(0, 1), (1, 3)]; + let bumped_unretained = [(0, 2), (1, 3)]; + let bumped_retained = [(0, 1), (1, 4)]; + + let hash_with = |generations: &[(SourceId, u64)]| { + let scope = scope_fixture(generations, &arena).nullified(ContextFeatures::all(), Some(&[1])); + let retained_scope_context = ContextImpl::root(&scope); + hash_of(&retained_scope_context) + }; + assert_eq!(hash_with(&initial), hash_with(&bumped_unretained), "unretained source bumps must not invalidate"); + assert_ne!(hash_with(&initial), hash_with(&bumped_retained), "retained source bumps must invalidate"); + } + + #[test] + fn excluding_keys_ignore_own_source_bumps() { + let arena = Arena::new(64).unwrap(); + let initial = [(7, 1), (9, 5)]; + let own_bumped = [(7, 2), (9, 5)]; + let other_bumped = [(7, 1), (9, 6)]; + + let hash_with = |generations: &[(SourceId, u64)]| { + let scope = scope_fixture(generations, &arena).excluding(7); + hash_of(&ContextImpl::root(&scope)) + }; + assert_eq!(hash_with(&initial), hash_with(&own_bumped), "a source's own generation bump must not change its slot key"); + assert_ne!(hash_with(&initial), hash_with(&other_bumped), "bumps of other sources must change the slot key"); + } + + #[test] + fn unretained_scope_sees_every_bump() { + let arena = Arena::new(64).unwrap(); + let initial = [(0, 1)]; + let bumped = [(0, 2)]; + let hash_with = |generations: &[(SourceId, u64)]| { + let scope = scope_fixture(generations, &arena); + hash_of(&ContextImpl::root(&scope)) + }; + assert_ne!(hash_with(&initial), hash_with(&bumped)); + } + + #[test] + fn set_index_is_visible_and_keeps_outer_levels() { + let arena = Arena::new(64).unwrap(); + let generations = []; + let scope = scope_fixture(&generations, &arena); + let root = ContextImpl::root(&scope); + let spilled = root.index_head(); + let mut ctx = root.promoted(&spilled, 0); + ctx.set_index(11); + let levels: Vec = ctx.try_index().unwrap().collect(); + assert_eq!(levels, vec![11, 0]); + } + + #[test] + fn vararg_chain_concatenates_innermost_first() { + let arena = Arena::new(64).unwrap(); + let generations = []; + let scope = scope_fixture(&generations, &arena); + let root = ContextImpl::root(&scope); + + let outer_value = 7u32; + let outer_args: [DynSlot; 1] = [&outer_value]; + let outer_link = VarArgLink { + args: VarArgSlots::Slice(&outer_args), + outer: None, + }; + let outer_ctx = root.with_varargs(&outer_link); + + let inner_value = String::from("inner"); + let inner_link = VarArgLink { + args: VarArgSlots::Single(&inner_value), + outer: Some(&outer_link), + }; + let inner_ctx = root.with_varargs(&inner_link); + + assert_eq!(root.varargs_len(), Err(VarArgsResult::NoVarArgs)); + assert_eq!(outer_ctx.varargs_len(), Ok(1)); + assert_eq!(inner_ctx.varargs_len(), Ok(2)); + assert!(inner_ctx.vararg(0).unwrap().downcast_ref::().is_some()); + assert!(inner_ctx.vararg(1).unwrap().downcast_ref::().is_some()); + assert!(matches!(inner_ctx.vararg(2), Err(VarArgsResult::IndexOutOfBounds))); + assert_ne!(hash_of(&outer_ctx), hash_of(&inner_ctx)); + } + + #[test] + fn snapshot_captures_vararg_levels() { + let arena = Arena::new(64).unwrap(); + let generations = []; + let scope = scope_fixture(&generations, &arena); + let root = ContextImpl::root(&scope); + + let outer_value = 7u32; + let outer_args: [DynSlot; 1] = [&outer_value]; + let outer_link = VarArgLink { + args: VarArgSlots::Slice(&outer_args), + outer: None, + }; + let inner_value = String::from("inner"); + let inner_link = VarArgLink { + args: VarArgSlots::Single(&inner_value), + outer: Some(&outer_link), + }; + let ctx = root.with_varargs(&inner_link); + + let snapshot = CtxSnapshot::capture(&ctx); + assert_eq!(snapshot.varargs_len(), Ok(2)); + assert_eq!(snapshot.vararg(0).unwrap().downcast_ref::(), Some(&inner_value)); + assert_eq!(snapshot.vararg(1).unwrap().downcast_ref::(), Some(&outer_value)); + assert!(matches!(snapshot.vararg(2), Err(VarArgsResult::IndexOutOfBounds))); + + let hash_via = |target: &dyn Fn(&mut dyn Hasher)| { + let mut hasher = std::hash::DefaultHasher::new(); + target(&mut hasher); + hasher.finish() + }; + assert_eq!( + hash_via(&|hasher| snapshot.hash_varargs(hasher)), + hash_via(&|hasher| ctx.hash_varargs(hasher)), + "snapshot varargs must hash like the borrowed chain" + ); + + let cloned = snapshot.clone(); + assert_eq!(cloned.vararg(0).unwrap().downcast_ref::(), Some(&inner_value)); + + let empty = CtxSnapshot::capture(&root); + assert_eq!(empty.varargs_len(), Err(VarArgsResult::NoVarArgs)); + assert!(matches!(empty.vararg(0), Err(VarArgsResult::NoVarArgs))); + } + + #[test] + fn sources_are_normalized_regardless_of_insertion_order() { + let mut dependencies = ContextDependencies::default(); + dependencies.add_sources(&[9, 3, 9]); + dependencies.add_sources(&[5, 1]); + assert_eq!(dependencies.sources(), &[1, 3, 5, 9]); + + let modification = ContextModification::from_sources(ContextFeatures::empty(), &[7, 2, 7]); + assert_eq!(modification.sources(), &[2, 7]); + } + + #[test] + fn a_snapshot_preserves_an_absent_position_axis() { + let arena = Arena::new(64).unwrap(); + let generations = []; + let scope = scope_fixture(&generations, &arena); + let root = ContextImpl::root(&scope); + + let absent = CtxSnapshot::capture(&root); + assert!(absent.try_position().is_none(), "capturing an unpositioned context must not invent a position stack"); + + let position = DVec2::new(1., 2.); + let positioned = CtxSnapshot::capture(&root.with_position(&PositionLink { position, outer: None })); + assert_eq!(positioned.try_position().map(|p| p.collect::>()), Some(vec![position])); + } + + #[test] + fn scope_arena_reaches_kernels_through_extract_arena() { + let arena = Arena::new(1024).unwrap(); + let generations = []; + let scope = scope_fixture(&generations, &arena); + let ctx = ContextImpl::root(&scope); + let (value, _) = ExtractArena::arena(&ctx).alloc(41u32).unwrap(); + assert_eq!(*value, 41); + } +} diff --git a/node-graph/libraries/core-types/src/frame_table.rs b/node-graph/libraries/core-types/src/frame_table.rs new file mode 100644 index 0000000000..31a1da78be --- /dev/null +++ b/node-graph/libraries/core-types/src/frame_table.rs @@ -0,0 +1,198 @@ +use crate::gpoll::Finality; +use std::cell::UnsafeCell; +use std::mem::{ManuallyDrop, MaybeUninit}; +use std::sync::atomic::{AtomicU8, AtomicU64, Ordering}; + +const SLOT_EMPTY: u8 = 0; +const SLOT_FINAL: u8 = 1; +const SLOT_PARTIAL: u8 = 2; + +pub struct FrameTable { + slots: [FrameSlot; CAP], +} + +struct FrameSlot { + key: AtomicU64, + state: AtomicU8, + value: UnsafeCell>, +} + +// SAFETY: the key CAS reserves a slot for one writer, and the Release store of its +// state publishes the value to every Acquire load in `lookup`, so concurrent access +// is ordered. Sharing the table hands out `&T` and drops `T` on whichever thread +// drops the table, which is what the `Send + Sync` bounds cover. +unsafe impl Sync for FrameTable {} +unsafe impl Send for FrameTable {} + +pub enum Lookup<'t, T> { + Hit(Finality, &'t T), + Vacant(VacantSlot<'t, T>), + Full, +} + +pub struct VacantSlot<'t, T> { + slot: &'t FrameSlot, +} + +impl Default for FrameTable { + fn default() -> Self { + Self::new() + } +} + +impl FrameTable { + pub fn new() -> Self { + Self { + slots: std::array::from_fn(|_| FrameSlot { + key: AtomicU64::new(0), + state: AtomicU8::new(SLOT_EMPTY), + value: UnsafeCell::new(MaybeUninit::uninit()), + }), + } + } + + pub fn lookup(&self, hash: u64) -> Lookup<'_, T> { + // Only hash 0 is remapped, so distinct hashes stay distinct keys. + let key = if hash == 0 { 1 } else { hash }; + for probe in 0..CAP { + let slot = &self.slots[(key as usize).wrapping_add(probe) % CAP]; + let stored = slot.key.load(Ordering::Acquire); + if stored == key { + return match slot.state.load(Ordering::Acquire) { + // SAFETY: a published state was stored with Release after the + // value write; the Acquire load above ordered that write. + SLOT_FINAL => Lookup::Hit(Finality::AllFinal, unsafe { (*slot.value.get()).assume_init_ref() }), + SLOT_PARTIAL => Lookup::Hit(Finality::Partial, unsafe { (*slot.value.get()).assume_init_ref() }), + _ => Lookup::Full, + }; + } + if stored == 0 && slot.key.compare_exchange(0, key, Ordering::AcqRel, Ordering::Acquire).is_ok() { + return Lookup::Vacant(VacantSlot { slot }); + } + } + Lookup::Full + } +} + +impl Drop for FrameTable { + fn drop(&mut self) { + for slot in &self.slots { + if slot.state.load(Ordering::Acquire) != SLOT_EMPTY { + // SAFETY: a non-empty state is only ever stored after the value + // write in `publish`. + unsafe { (*slot.value.get()).assume_init_drop() } + } + } + } +} + +impl<'t, T> VacantSlot<'t, T> { + pub fn publish(self, value: T, finality: Finality) -> &'t T { + let slot = ManuallyDrop::new(self).slot; + // SAFETY: the CAS in `lookup` reserved this slot exclusively for us and + // its state is still SLOT_EMPTY, so nobody reads the value yet. + let lent = unsafe { &*(*slot.value.get()).write(value) }; + let state = match finality { + Finality::AllFinal => SLOT_FINAL, + Finality::Partial => SLOT_PARTIAL, + }; + slot.state.store(state, Ordering::Release); + lent + } + + pub fn release(self) { + drop(self); + } +} + +/// Frees the reservation, so an early return or panic between `lookup` and +/// `publish` cannot retire the slot for the rest of the table's life. +impl Drop for VacantSlot<'_, T> { + fn drop(&mut self) { + self.slot.key.store(0, Ordering::Release); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::atomic::AtomicU32; + + #[test] + fn publish_then_hit_with_finality() { + let table = FrameTable::::new(); + let Lookup::Vacant(slot) = table.lookup(7) else { + panic!("fresh table must be vacant"); + }; + assert_eq!(*slot.publish(41, Finality::Partial), 41); + let Lookup::Hit(finality, value) = table.lookup(7) else { + panic!("published key must hit"); + }; + assert_eq!((finality, *value), (Finality::Partial, 41)); + } + + #[test] + fn released_slot_is_vacant_again() { + let table = FrameTable::::new(); + let Lookup::Vacant(slot) = table.lookup(7) else { unreachable!() }; + slot.release(); + assert!(matches!(table.lookup(7), Lookup::Vacant(_))); + } + + #[test] + fn a_dropped_reservation_frees_the_slot() { + let table = FrameTable::::new(); + let Lookup::Vacant(slot) = table.lookup(7) else { unreachable!() }; + drop(slot); + assert!(matches!(table.lookup(7), Lookup::Vacant(_)), "an abandoned reservation must not retire the slot"); + } + + #[test] + fn neighboring_hashes_do_not_share_an_entry() { + let table = FrameTable::::new(); + let Lookup::Vacant(slot) = table.lookup(6) else { unreachable!() }; + slot.publish(600, Finality::AllFinal); + assert!(matches!(table.lookup(7), Lookup::Vacant(_)), "an even hash must not answer for its odd neighbor"); + } + + #[test] + fn the_zero_hash_round_trips() { + let table = FrameTable::::new(); + let Lookup::Vacant(slot) = table.lookup(0) else { unreachable!() }; + slot.publish(11, Finality::AllFinal); + let Lookup::Hit(_, value) = table.lookup(0) else { + panic!("the remapped sentinel hash must still hit"); + }; + assert_eq!(*value, 11); + } + + #[test] + fn distinct_keys_probe_past_collisions_until_full() { + let table = FrameTable::::new(); + for hash in [2, 4, 6, 8] { + let Lookup::Vacant(slot) = table.lookup(hash) else { + panic!("hash {hash} should find a vacant slot"); + }; + slot.publish(hash as u32, Finality::AllFinal); + } + assert!(matches!(table.lookup(100), Lookup::Full)); + } + + #[test] + fn drop_runs_glue_for_published_values_only() { + static DROPS: AtomicU32 = AtomicU32::new(0); + struct Probe; + impl Drop for Probe { + fn drop(&mut self) { + DROPS.fetch_add(1, Ordering::Relaxed); + } + } + let table = FrameTable::::new(); + let Lookup::Vacant(slot) = table.lookup(1) else { unreachable!() }; + slot.publish(Probe, Finality::AllFinal); + let Lookup::Vacant(reserved_but_unpublished) = table.lookup(2) else { unreachable!() }; + reserved_but_unpublished.release(); + drop(table); + assert_eq!(DROPS.load(Ordering::Relaxed), 1); + } +} diff --git a/node-graph/libraries/core-types/src/generic.rs b/node-graph/libraries/core-types/src/generic.rs deleted file mode 100644 index 055c0cb53d..0000000000 --- a/node-graph/libraries/core-types/src/generic.rs +++ /dev/null @@ -1,17 +0,0 @@ -use crate::Node; -use std::marker::PhantomData; -#[derive(Clone)] -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 { - type Output = O; - fn eval(&'i self, input: I) -> Self::Output { - self.0(input) - } -} - -impl O, I, O> FnNode { - pub fn new(f: T) -> Self { - FnNode(f, PhantomData) - } -} diff --git a/node-graph/libraries/core-types/src/gpoll.rs b/node-graph/libraries/core-types/src/gpoll.rs new file mode 100644 index 0000000000..547103b9d8 --- /dev/null +++ b/node-graph/libraries/core-types/src/gpoll.rs @@ -0,0 +1,235 @@ +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ErrorKind { + Node(&'static str), + ArenaExhausted, + Panic, +} + +impl PartialEq<&str> for ErrorKind { + fn eq(&self, other: &&str) -> bool { + matches!(self, ErrorKind::Node(kind) if kind == other) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct GraphError { + pub kind: ErrorKind, + pub trace: Vec, +} + +impl GraphError { + pub fn new(kind: &'static str) -> Self { + Self { + kind: ErrorKind::Node(kind), + trace: Vec::new(), + } + } + + pub fn traced(mut self, input_index: usize) -> Self { + self.trace.push(input_index); + self + } +} + +#[derive(Clone, Debug, PartialEq)] +pub enum GPoll { + Pending, + Final(T), + Partial(T), + Fallback(Box<(T, GraphError)>), + Error(Box), +} + +impl GPoll { + #[inline(always)] + pub fn map(self, f: impl FnOnce(T) -> U) -> GPoll { + match self { + GPoll::Pending => GPoll::Pending, + GPoll::Final(value) => GPoll::Final(f(value)), + GPoll::Partial(value) => GPoll::Partial(f(value)), + GPoll::Fallback(boxed) => { + let (value, e) = *boxed; + GPoll::Fallback(Box::new((f(value), e))) + } + GPoll::Error(e) => GPoll::Error(e), + } + } + + #[inline(always)] + pub fn and_then(self, f: impl FnOnce(T) -> GPoll) -> GPoll { + match self { + GPoll::Pending => GPoll::Pending, + GPoll::Final(value) => f(value), + GPoll::Partial(value) => match f(value) { + GPoll::Final(result) => GPoll::Partial(result), + other => other, + }, + GPoll::Fallback(boxed) => { + let (value, e) = *boxed; + match f(value) { + GPoll::Pending => GPoll::Pending, + GPoll::Final(result) | GPoll::Partial(result) => GPoll::Fallback(Box::new((result, e))), + GPoll::Fallback(inner) => { + let (result, _) = *inner; + GPoll::Fallback(Box::new((result, e))) + } + GPoll::Error(inner) => GPoll::Error(inner), + } + } + GPoll::Error(e) => GPoll::Error(e), + } + } + + #[inline(always)] + pub fn zip(self, other: GPoll) -> GPoll<(T, U)> { + match (self, other) { + (GPoll::Error(e), _) | (_, GPoll::Error(e)) => GPoll::Error(e), + (GPoll::Pending, _) | (_, GPoll::Pending) => GPoll::Pending, + (GPoll::Final(a), GPoll::Final(b)) => GPoll::Final((a, b)), + (GPoll::Fallback(boxed), GPoll::Final(b) | GPoll::Partial(b)) => { + let (a, e) = *boxed; + GPoll::Fallback(Box::new(((a, b), e))) + } + (GPoll::Final(a) | GPoll::Partial(a), GPoll::Fallback(boxed)) => { + let (b, e) = *boxed; + GPoll::Fallback(Box::new(((a, b), e))) + } + (GPoll::Fallback(first), GPoll::Fallback(second)) => { + let (a, e) = *first; + let (b, _) = *second; + GPoll::Fallback(Box::new(((a, b), e))) + } + (GPoll::Partial(a), GPoll::Final(b) | GPoll::Partial(b)) | (GPoll::Final(a), GPoll::Partial(b)) => GPoll::Partial((a, b)), + } + } + + #[inline(always)] + pub fn trace(self, input: usize) -> Self { + match self { + GPoll::Fallback(mut boxed) => { + boxed.1.trace.push(input); + GPoll::Fallback(boxed) + } + GPoll::Error(mut e) => { + e.trace.push(input); + GPoll::Error(e) + } + other => other, + } + } + + pub fn fallback(value: T, kind: &'static str) -> Self { + GPoll::Fallback(Box::new((value, GraphError::new(kind)))) + } + + pub fn error(kind: &'static str) -> Self { + GPoll::Error(Box::new(GraphError::new(kind))) + } + + pub fn arena_exhausted() -> Self { + GPoll::Error(Box::new(GraphError { + kind: ErrorKind::ArenaExhausted, + trace: Vec::new(), + })) + } + + pub fn panicked() -> Self { + GPoll::Error(Box::new(GraphError { + kind: ErrorKind::Panic, + trace: Vec::new(), + })) + } +} + +#[derive(Clone, Debug, PartialEq)] +pub enum Interrupt { + Pending, + Error(Box), +} + +impl From for Interrupt { + fn from(error: GraphError) -> Self { + Interrupt::Error(Box::new(error)) + } +} + +impl From for GPoll { + fn from(interrupt: Interrupt) -> Self { + match interrupt { + Interrupt::Pending => GPoll::Pending, + Interrupt::Error(e) => GPoll::Error(e), + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Extent { + Free, + Exactly(usize), +} + +impl Extent { + pub fn meet(a: GPoll, b: GPoll) -> GPoll { + a.zip(b).and_then(|(a, b)| match (a, b) { + (Extent::Free, other) | (other, Extent::Free) => GPoll::Final(other), + (Extent::Exactly(n), Extent::Exactly(m)) if n == m => GPoll::Final(Extent::Exactly(n)), + (Extent::Exactly(n), Extent::Exactly(m)) => GPoll::fallback(Extent::Exactly(n.min(m)), "extent mismatch"), + }) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Finality { + AllFinal, + Partial, +} + +impl Finality { + pub fn meet(self, other: Finality) -> Finality { + match (self, other) { + (Finality::AllFinal, Finality::AllFinal) => Finality::AllFinal, + _ => Finality::Partial, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn free_is_the_meet_identity() { + let meet = Extent::meet(GPoll::Final(Extent::Free), GPoll::Final(Extent::Exactly(4))); + assert_eq!(meet, GPoll::Final(Extent::Exactly(4))); + } + + #[test] + fn extent_mismatch_truncates_and_reports() { + let meet = Extent::meet(GPoll::Final(Extent::Exactly(3)), GPoll::Final(Extent::Exactly(5))); + let GPoll::Fallback(boxed) = meet else { + panic!("expected fallback, got {meet:?}"); + }; + assert_eq!(boxed.0, Extent::Exactly(3)); + assert!(boxed.1.kind == "extent mismatch"); + } + + #[test] + fn error_dominates_pending_in_zip() { + let zipped = GPoll::::error("boom").zip(GPoll::::Pending); + assert!(matches!(zipped, GPoll::Error(_))); + } + + #[test] + fn trace_builds_root_to_source_path() { + let poll = GPoll::::error("boom").trace(2).trace(0); + let GPoll::Error(e) = poll else { unreachable!() }; + assert_eq!(e.trace, vec![2, 0]); + } + + #[test] + fn interrupt_round_trips_to_gpoll() { + assert_eq!(GPoll::::from(Interrupt::Pending), GPoll::Pending); + let interrupt = Interrupt::from(GraphError::new("boom")); + assert!(matches!(GPoll::::from(interrupt), GPoll::Error(e) if e.kind == "boom")); + } +} diff --git a/node-graph/libraries/core-types/src/lib.rs b/node-graph/libraries/core-types/src/lib.rs index 878e7f5360..5906c6ab65 100644 --- a/node-graph/libraries/core-types/src/lib.rs +++ b/node-graph/libraries/core-types/src/lib.rs @@ -1,16 +1,20 @@ extern crate log; +pub mod arena; pub mod bounds; pub mod consts; pub mod context; -pub mod generic; +pub mod frame_table; +pub mod gpoll; pub mod list; pub mod math; pub mod memo; pub mod misc; +pub mod node; pub mod ops; pub mod registry; pub mod render_complexity; +pub mod runtime; pub mod transform; pub mod uuid; pub mod value; @@ -34,113 +38,15 @@ pub use no_std_types::blending; pub use no_std_types::choice_type; pub use no_std_types::color; pub use no_std_types::shaders; +pub use node::Node; pub use num_traits; -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; - /// Evaluates the node with the single specified input. - fn eval(&'i self, input: Input) -> Self::Output; - /// Resets the node, e.g. the LetNode's cache is set to None. - fn reset(&self) {} - /// Returns the name of the node for diagnostic purposes. - fn node_name(&self) -> &'static str { - std::any::type_name::() - } - /// Serialize the node which is used for the `introspect` function which can retrieve values from monitor nodes. - fn serialize(&self) -> Option> { - log::warn!("Node::serialize not implemented for {}", std::any::type_name::()); - None - } -} - mod types; pub use types::*; -pub trait NodeIO<'i, Input>: Node<'i, Input> -where - Self::Output: 'i + StaticTypeSized, - Input: StaticTypeSized, -{ - fn input_type(&self) -> TypeId { - TypeId::of::() - } - fn input_type_name(&self) -> &'static str { - std::any::type_name::() - } - fn output_type(&self) -> TypeId { - TypeId::of::<::Static>() - } - fn output_type_name(&self) -> &'static str { - std::any::type_name::() - } - fn to_node_io(&self, inputs: Vec) -> NodeIOTypes { - NodeIOTypes { - call_argument: concrete!(::Static), - return_value: concrete!(::Static), - 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 -where - N::Output: 'i + StaticTypeSized, - I: StaticTypeSized, -{ -} - -impl<'i, I: 'i, N: Node<'i, I> + ?Sized> Node<'i, I> for &'i 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 { - (**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) - } -} - -impl<'i, I, O: 'i> Node<'i, I> for Pin + 'i>> { - type Output = O; - fn eval(&'i self, input: I) -> O { - (**self).eval(input) - } -} -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 { - (**self).eval(input) - } -} - pub trait InputAccessorSource<'a, T>: InputAccessorSourceIdentifier + std::fmt::Debug { fn get_input(&'a self, index: usize) -> Option<&'a T>; fn set_input(&'a mut self, index: usize, value: T); diff --git a/node-graph/libraries/core-types/src/node.rs b/node-graph/libraries/core-types/src/node.rs new file mode 100644 index 0000000000..23abf38d7c --- /dev/null +++ b/node-graph/libraries/core-types/src/node.rs @@ -0,0 +1,424 @@ +use crate::context::InjectIndex; +use crate::gpoll::{Extent, Finality, GPoll, GraphError, Interrupt}; +use std::cell::Cell; +use std::mem::MaybeUninit; +use std::ops::Range; + +#[derive(Debug)] +pub enum BatchStatus<'a, T> { + Lent(&'a [T], Finality), + Filled(FilledBatch<'a, T>, Finality), + Pending, + Error(GraphError), + NeedBuffer, + InvalidRange, +} + +/// Owns the initialized prefix of a caller-supplied scratch buffer, dropping every +/// lane unless [`FilledBatch::into_values`] hands the obligation back to the caller. +#[derive(Debug)] +pub struct FilledBatch<'a, T> { + values: &'a mut [T], +} + +impl<'a, T> FilledBatch<'a, T> { + /// # Safety + /// + /// The first `len` elements of `scratch` must be initialized, and `len` must not exceed `scratch.len()`. + pub unsafe fn new(scratch: &'a mut [MaybeUninit], len: usize) -> Self { + Self { + values: unsafe { assume_init_prefix_mut(scratch, len) }, + } + } + + pub fn values(&self) -> &[T] { + self.values + } + + pub fn into_values(self) -> &'a mut [T] { + let mut guard = std::mem::ManuallyDrop::new(self); + std::mem::take(&mut guard.values) + } +} + +impl Drop for FilledBatch<'_, T> { + fn drop(&mut self) { + // SAFETY: every lane was initialized when the guard was built and none has + // been moved out, since `into_values` consumes the guard instead. + unsafe { std::ptr::drop_in_place(self.values as *mut [T]) } + } +} + +/// # Safety +/// +/// The first `len` elements of `scratch` must be initialized, and `len` must not exceed `scratch.len()`. +pub unsafe fn assume_init_prefix_mut(scratch: &mut [MaybeUninit], len: usize) -> &mut [T] { + debug_assert!(len <= scratch.len()); + unsafe { std::slice::from_raw_parts_mut(scratch.as_mut_ptr().cast::(), len) } +} + +pub trait Node { + type Output; + + fn eval(&self, input: &Input) -> GPoll; + + fn extent(&self, _input: &Input) -> GPoll { + GPoll::Final(Extent::Free) + } + + /// Introspection access to node-resident records; `None` for ordinary nodes. + fn serialize(&self) -> Option> { + None + } + + fn eval_batch<'a>(&self, input: &'a Input, range: Range, scratch: Option<&'a mut [MaybeUninit]>) -> BatchStatus<'a, Self::Output> + where + Input: InjectIndex + Copy, + { + let Some(scratch) = scratch else { + return BatchStatus::NeedBuffer; + }; + let Some(len) = range.end.checked_sub(range.start).and_then(|len| usize::try_from(len).ok()) else { + return BatchStatus::InvalidRange; + }; + if scratch.len() < len { + return BatchStatus::InvalidRange; + } + let mut local = *input; + let mut finality = Finality::AllFinal; + for offset in 0..len { + local.set_index(range.start + offset as u64); + let abort = match self.eval(&local) { + GPoll::Final(value) => { + scratch[offset].write(value); + None + } + GPoll::Partial(value) => { + scratch[offset].write(value); + finality = Finality::Partial; + None + } + GPoll::Pending => Some(BatchStatus::Pending), + GPoll::Fallback(boxed) => Some(BatchStatus::Error(boxed.1)), + GPoll::Error(e) => Some(BatchStatus::Error(*e)), + }; + if let Some(status) = abort { + for written in scratch[..offset].iter_mut() { + // SAFETY: every lane before `offset` was written by this loop. + unsafe { written.assume_init_drop() }; + } + return status; + } + } + // SAFETY: all `len` lanes were written by the loop above. + BatchStatus::Filled(unsafe { FilledBatch::new(scratch, len) }, finality) + } +} + +impl Node for &N +where + N: Node + ?Sized, +{ + type Output = N::Output; + + fn eval(&self, input: &Input) -> GPoll { + (**self).eval(input) + } + + fn extent(&self, input: &Input) -> GPoll { + (**self).extent(input) + } + + fn serialize(&self) -> Option> { + (**self).serialize() + } + + fn eval_batch<'a>(&self, input: &'a Input, range: Range, scratch: Option<&'a mut [MaybeUninit]>) -> BatchStatus<'a, Self::Output> + where + Input: InjectIndex + Copy, + { + (**self).eval_batch(input, range, scratch) + } +} + +impl Node for Box +where + N: Node + ?Sized, +{ + type Output = N::Output; + + fn eval(&self, input: &Input) -> GPoll { + (**self).eval(input) + } + + fn extent(&self, input: &Input) -> GPoll { + (**self).extent(input) + } + + fn serialize(&self) -> Option> { + (**self).serialize() + } + + fn eval_batch<'a>(&self, input: &'a Input, range: Range, scratch: Option<&'a mut [MaybeUninit]>) -> BatchStatus<'a, Self::Output> + where + Input: InjectIndex + Copy, + { + (**self).eval_batch(input, range, scratch) + } +} + +impl Node for std::sync::Arc +where + N: Node + ?Sized, +{ + type Output = N::Output; + + fn eval(&self, input: &Input) -> GPoll { + (**self).eval(input) + } + + fn extent(&self, input: &Input) -> GPoll { + (**self).extent(input) + } + + fn serialize(&self) -> Option> { + (**self).serialize() + } + + fn eval_batch<'a>(&self, input: &'a Input, range: Range, scratch: Option<&'a mut [MaybeUninit]>) -> BatchStatus<'a, Self::Output> + where + Input: InjectIndex + Copy, + { + (**self).eval_batch(input, range, scratch) + } +} + +pub struct StatusCell { + finality: Cell, + error: Cell>, + no_partial: bool, +} + +impl Default for StatusCell { + fn default() -> Self { + Self::new() + } +} + +impl StatusCell { + pub fn new() -> Self { + Self { + finality: Cell::new(Finality::AllFinal), + error: Cell::new(None), + no_partial: false, + } + } + + pub fn no_partial() -> Self { + Self { no_partial: true, ..Self::new() } + } + + pub fn eval_input>(&self, input_index: usize, node: &N, input: &Input) -> Result { + match node.eval(input) { + GPoll::Final(value) => Ok(value), + GPoll::Partial(_) if self.no_partial => Err(Interrupt::Pending), + GPoll::Partial(value) => { + self.finality.set(Finality::Partial); + Ok(value) + } + GPoll::Fallback(boxed) => { + let (value, error) = *boxed; + let first = self.error.take(); + self.error.set(first.or(Some(error.traced(input_index)))); + Ok(value) + } + GPoll::Pending => Err(Interrupt::Pending), + GPoll::Error(mut error) => { + error.trace.push(input_index); + Err(Interrupt::Error(error)) + } + } + } + + pub fn finish(self, value: T) -> GPoll { + match (self.error.take(), self.finality.get()) { + (Some(error), _) => GPoll::Fallback(Box::new((value, error))), + (None, Finality::AllFinal) => GPoll::Final(value), + (None, Finality::Partial) => GPoll::Partial(value), + } + } + + pub fn merge(self, poll: GPoll) -> GPoll { + match poll { + GPoll::Final(value) => self.finish(value), + GPoll::Partial(_) if self.no_partial => GPoll::Pending, + GPoll::Partial(value) => match self.finish(value) { + GPoll::Final(value) => GPoll::Partial(value), + other => other, + }, + GPoll::Fallback(boxed) => { + let (value, error) = *boxed; + let first = self.error.take().unwrap_or(error); + GPoll::Fallback(Box::new((value, first))) + } + interrupted => interrupted, + } + } +} + +#[derive(Clone, Copy)] +pub struct LazyInput<'a, N> { + node: &'a N, + cell: &'a StatusCell, + input_index: usize, +} + +impl<'a, N> LazyInput<'a, N> { + pub fn new(node: &'a N, cell: &'a StatusCell, input_index: usize) -> Self { + Self { node, cell, input_index } + } + + pub fn eval(&self, ctx: &Input) -> Result + where + N: Node, + { + self.cell.eval_input(self.input_index, self.node, ctx) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::atomic::{AtomicU32, Ordering}; + + #[derive(Clone, Copy)] + struct TestInput { + index: u64, + } + + impl InjectIndex for TestInput { + fn set_index(&mut self, index: u64) { + self.index = index; + } + } + + struct Double; + + impl Node for Double { + type Output = u64; + + fn eval(&self, input: &TestInput) -> GPoll { + GPoll::Final(input.index * 2) + } + } + + #[test] + fn spec_loop_fills_scratch_per_lane() { + let input = TestInput { index: 0 }; + let mut scratch = [const { MaybeUninit::uninit() }; 4]; + let status = Double.eval_batch(&input, 2..6, Some(&mut scratch)); + let BatchStatus::Filled(lanes, finality) = status else { + panic!("expected filled, got {status:?}"); + }; + assert_eq!(lanes.values(), &[4, 6, 8, 10]); + assert_eq!(finality, Finality::AllFinal); + } + + #[test] + fn a_dropped_filled_batch_reclaims_every_lane() { + static DROPS: AtomicU32 = AtomicU32::new(0); + #[derive(Clone)] + struct Probe; + impl Drop for Probe { + fn drop(&mut self) { + DROPS.fetch_add(1, Ordering::Relaxed); + } + } + struct Probes; + impl Node for Probes { + type Output = Probe; + + fn eval(&self, _input: &TestInput) -> GPoll { + GPoll::Final(Probe) + } + } + + let input = TestInput { index: 0 }; + let mut scratch = [const { MaybeUninit::uninit() }; 3]; + let status = Probes.eval_batch(&input, 0..3, Some(&mut scratch)); + assert!(matches!(status, BatchStatus::Filled(..))); + drop(status); + assert_eq!(DROPS.load(Ordering::Relaxed), 3, "an unconsumed batch must not leak its lanes"); + } + + #[test] + fn probe_without_scratch_requests_a_buffer() { + let input = TestInput { index: 0 }; + assert!(matches!(Double.eval_batch(&input, 0..4, None), BatchStatus::NeedBuffer)); + } + + #[test] + fn undersized_scratch_is_an_invalid_range() { + let input = TestInput { index: 0 }; + let mut scratch = [const { MaybeUninit::uninit() }; 2]; + assert!(matches!(Double.eval_batch(&input, 0..4, Some(&mut scratch)), BatchStatus::InvalidRange)); + } + + #[test] + fn partial_lane_downgrades_batch_finality() { + struct PartialAtThree; + impl Node for PartialAtThree { + type Output = u64; + fn eval(&self, input: &TestInput) -> GPoll { + match input.index { + 3 => GPoll::Partial(input.index), + index => GPoll::Final(index), + } + } + } + let input = TestInput { index: 0 }; + let mut scratch = [const { MaybeUninit::uninit() }; 4]; + let status = PartialAtThree.eval_batch(&input, 0..4, Some(&mut scratch)); + let BatchStatus::Filled(lanes, finality) = status else { + panic!("expected filled, got {status:?}"); + }; + assert_eq!(lanes.values(), &[0, 1, 2, 3]); + assert_eq!(finality, Finality::Partial); + } + + #[test] + fn abort_drops_already_written_lanes() { + static DROPS: AtomicU32 = AtomicU32::new(0); + struct Probe; + impl Drop for Probe { + fn drop(&mut self) { + DROPS.fetch_add(1, Ordering::Relaxed); + } + } + struct PendingAtTwo; + impl Node for PendingAtTwo { + type Output = Probe; + fn eval(&self, input: &TestInput) -> GPoll { + match input.index { + 2 => GPoll::Pending, + _ => GPoll::Final(Probe), + } + } + } + let input = TestInput { index: 0 }; + let mut scratch = [const { MaybeUninit::uninit() }; 4]; + let status = PendingAtTwo.eval_batch(&input, 0..4, Some(&mut scratch)); + assert!(matches!(status, BatchStatus::Pending)); + assert_eq!(DROPS.load(Ordering::Relaxed), 2); + } + + #[test] + fn trait_is_object_safe_across_erased_edges() { + let erased: Box> = Box::new(Double); + let input = TestInput { index: 21 }; + assert_eq!(erased.eval(&input), GPoll::Final(42)); + let mut scratch = [const { MaybeUninit::uninit() }; 2]; + let status = erased.eval_batch(&input, 0..2, Some(&mut scratch)); + assert!(matches!(status, BatchStatus::Filled(_, Finality::AllFinal))); + } +} diff --git a/node-graph/libraries/core-types/src/ops.rs b/node-graph/libraries/core-types/src/ops.rs index 9d812c9576..afc871dbd5 100644 --- a/node-graph/libraries/core-types/src/ops.rs +++ b/node-graph/libraries/core-types/src/ops.rs @@ -1,56 +1,26 @@ -use crate::Node; use crate::list::{Attribute, AttributeDyn, AttributeValueDyn, Item, List, ListDyn}; use crate::transform::Footprint; use glam::DVec2; use graphene_hash::CacheHash; -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 -where - N: for<'n> Node<'n, I, Output = O>, -{ - type Output = O; - fn eval(&'i self, input: I) -> Self::Output { - 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> { - pub fn new(node: N) -> Self { - Self(node, PhantomData) - } -} -impl<'i, N: for<'a> Node<'a, I> + Clone, I: '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> {} /// 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; +} + +/// The asynchronous counterpart of [`Convert`]; a conversion pair implements exactly one of the two traits. +pub trait ConvertAsync: Sized { + #[must_use] + fn convert(self, footprint: Footprint, converter: C) -> crate::runtime::SourceFuture; } impl Convert for T { /// Converts this type into a `String` using its `ToString` implementation. #[inline] - async fn convert(self, _: Footprint, _converter: ()) -> String { + fn convert(self, _: Footprint, _converter: ()) -> String { self.to_string() } } @@ -60,7 +30,7 @@ pub trait ListConvert { } impl + Send> Convert, ()> for List { - async fn convert(self, _: Footprint, _: ()) -> List { + fn convert(self, _: Footprint, _: ()) -> List { let list: List = self .into_iter() .map(|row| { @@ -76,7 +46,7 @@ impl + Send> Convert, ()> for List { /// from any `List` express their signature as `AttributeDyn` and avoid monomorphizing /// over `U`; the compiler inserts this convert to bridge concrete-typed graph wires to the dyn input. impl Convert for List { - async fn convert(self, _: Footprint, _: ()) -> AttributeDyn { + fn convert(self, _: Footprint, _: ()) -> AttributeDyn { let values: Vec = self.into_iter().map(|row| row.into_element()).collect(); AttributeDyn(Box::new(Attribute(values))) } @@ -86,7 +56,7 @@ impl Convert for T { - async fn convert(self, _: Footprint, _: ()) -> AttributeValueDyn { + fn convert(self, _: Footprint, _: ()) -> AttributeValueDyn { AttributeValueDyn(Box::new(self)) } } @@ -95,13 +65,13 @@ impl Convert for List { - async fn convert(self, _: Footprint, _: ()) -> ListDyn { + fn convert(self, _: Footprint, _: ()) -> ListDyn { self.into() } } impl Convert for DVec2 { - async fn convert(self, _: Footprint, _: ()) -> DVec2 { + fn convert(self, _: Footprint, _: ()) -> DVec2 { self } } @@ -115,7 +85,7 @@ pub trait FromAnchorPosition { // Converts a position into a vector path composed of a single anchor point impl Convert, ()> for DVec2 { - async fn convert(self, _: Footprint, _: ()) -> List { + fn convert(self, _: Footprint, _: ()) -> List { List::new_from_item(Item::new_from_element(T::from_anchor_position(self))) } } @@ -124,7 +94,7 @@ impl Convert, ()> for DVec2 { 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 } } @@ -146,7 +116,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) } } diff --git a/node-graph/libraries/core-types/src/registry.rs b/node-graph/libraries/core-types/src/registry.rs index 0d3e17cdbd..18e32700e7 100644 --- a/node-graph/libraries/core-types/src/registry.rs +++ b/node-graph/libraries/core-types/src/registry.rs @@ -1,10 +1,12 @@ -use crate::{ContextFeature, Node, NodeIO, NodeIOTypes, ProtoNodeIdentifier, Type, WasmNotSend}; -use dyn_any::{DynAny, StaticType}; +use crate::concrete; +use crate::context::{Context, ContextImpl}; +use crate::node::Node; +use crate::{ContextFeature, ProtoNodeIdentifier, Type, WasmNotSend, WasmNotSync}; +use dyn_any::DynAny; +use graphene_hash::CacheHash; pub use no_std_types::registry::types; use std::collections::HashMap; -use std::marker::PhantomData; -use std::ops::Deref; -use std::pin::Pin; +use std::hash::Hasher; use std::sync::{LazyLock, Mutex}; // Translation struct between macro and definition @@ -18,6 +20,8 @@ pub struct NodeMetadata { pub context_features: Vec, pub memoize: bool, pub inject_scope: bool, + /// The macro appended its hidden `_runtime` and `_source` fields as the last two entries of `fields`. + pub async_source_fields: bool, } // Translation struct between macro and definition @@ -55,239 +59,457 @@ pub enum RegistryValueSource { None, Default(&'static str), Scope(&'static str), + SourceId, } -type NodeRegistry = LazyLock>>>; +type NodeRegistry = LazyLock>>>; pub static NODE_REGISTRY: NodeRegistry = LazyLock::new(|| Mutex::new(HashMap::new())); pub static NODE_METADATA: LazyLock>> = LazyLock::new(|| Mutex::new(HashMap::new())); +pub use crate::NodeIOTypes; + #[cfg(not(target_family = "wasm"))] -pub type DynFuture<'n, T> = Pin + 'n + Send>>; +pub type ErasedNode = dyn for<'c> Node, Output = T> + Send + Sync; #[cfg(target_family = "wasm")] -pub type DynFuture<'n, T> = Pin + 'n>>; -pub type LocalFuture<'n, T> = Pin + 'n>>; +pub type ErasedNode = dyn for<'c> Node, Output = T>; #[cfg(not(target_family = "wasm"))] -pub type Any<'n> = Box + 'n + Send>; +pub type ErasedLendNode = dyn for<'c> Node, Output = &'c T> + Send + Sync; #[cfg(target_family = "wasm")] -pub type Any<'n> = Box + 'n>; -pub type FutureAny<'n> = DynFuture<'n, Any<'n>>; -// TODO: is this safe? This is assumed to be send+sync. +pub type ErasedLendNode = dyn for<'c> Node, Output = &'c T>; + #[cfg(not(target_family = "wasm"))] -pub type TypeErasedNode<'n> = dyn for<'i> NodeIO<'i, Any<'i>, Output = FutureAny<'i>> + 'n + Send + Sync; +type DynEdge = dyn std::any::Any + 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 TypeErasedRef<'n> = &'n TypeErasedNode<'n>; -pub type TypeErasedBox<'n> = Box>; -pub type TypeErasedPinned<'n> = Pin>>; +type DynEdge = dyn std::any::Any; -pub type SharedNodeContainer = std::sync::Arc; +pub fn edge_type() -> Type { + Type::Fn(Box::new(concrete!(Context)), Box::new(concrete!(T))) +} -pub type NodeConstructor = fn(Vec) -> DynFuture<'static, TypeErasedBox<'static>>; +pub fn lend_edge_type() -> Type { + Type::Fn(Box::new(concrete!(Context)), Box::new(Type::Ref(Box::new(concrete!(T))))) +} -#[derive(Clone)] -pub struct NodeContainer { - #[cfg(feature = "dealloc_nodes")] - pub node: *const TypeErasedNode<'static>, - #[cfg(not(feature = "dealloc_nodes"))] - pub node: TypeErasedRef<'static>, +pub fn cache_key(ctx: &C) -> u64 { + let mut hasher = graphene_hash::FxHasher64::new(); + ctx.cache_hash(&mut hasher); + hasher.finish() } -impl Deref for NodeContainer { - type Target = TypeErasedNode<'static>; +#[derive(Debug, PartialEq)] +pub enum ConstructionError { + Arity { expected: usize, got: usize }, + Type { expected: Box, found: Box }, +} - #[cfg(feature = "dealloc_nodes")] - fn deref(&self) -> &Self::Target { - unsafe { &*(self.node) } - #[cfg(not(feature = "dealloc_nodes"))] - self.node +pub struct SharedEdge { + ptr: std::ptr::NonNull, + own: std::sync::Arc, +} + +impl SharedEdge { + pub fn new(own: std::sync::Arc) -> Self { + Self { + ptr: std::ptr::NonNull::from(&*own), + own, + } } - #[cfg(not(feature = "dealloc_nodes"))] - fn deref(&self) -> &Self::Target { - self.node + + pub fn share(&self) -> Self { + Self { ptr: self.ptr, own: self.own.clone() } } } -/// # Safety -/// Marks NodeContainer as Sync. This dissallows the use of threadlocal storage for nodes as this would invalidate references to them. -// TODO: implement this on a higher level wrapper to avoid missuse -#[cfg(feature = "dealloc_nodes")] -unsafe impl Send for NodeContainer {} -#[cfg(feature = "dealloc_nodes")] -unsafe impl Sync for NodeContainer {} - -#[cfg(feature = "dealloc_nodes")] -impl Drop for NodeContainer { - fn drop(&mut self) { - unsafe { self.dealloc_unchecked() } +// SAFETY: `ptr` is derived from the owned Arc and never mutated through, so the edge is exactly as +// thread safe as the payload it shares. +unsafe impl Send for SharedEdge {} +// SAFETY: as in Send. +unsafe impl Sync for SharedEdge {} + +impl Node for SharedEdge +where + N: Node + ?Sized, +{ + type Output = N::Output; + + fn eval(&self, input: &Input) -> crate::gpoll::GPoll { + // SAFETY: `own` keeps the payload alive for `self`'s lifetime and Arc + // payloads are address stable. + unsafe { self.ptr.as_ref() }.eval(input) } + + fn extent(&self, input: &Input) -> crate::gpoll::GPoll { + // SAFETY: as in eval. + unsafe { self.ptr.as_ref() }.extent(input) + } + + fn serialize(&self) -> Option> { + // SAFETY: as in eval. + unsafe { self.ptr.as_ref() }.serialize() + } + + fn eval_batch<'a>(&self, input: &'a Input, range: std::ops::Range, scratch: Option<&'a mut [std::mem::MaybeUninit]>) -> crate::node::BatchStatus<'a, Self::Output> + where + Input: crate::context::InjectIndex + Copy, + { + // SAFETY: as in eval. + unsafe { self.ptr.as_ref() }.eval_batch(input, range, scratch) + } +} + +pub struct EdgeHandle { + node: Box, + share: fn(&DynEdge) -> Box, + serialize: fn(&DynEdge) -> Option>, + ty: Type, } -impl std::fmt::Debug for NodeContainer { +impl std::fmt::Debug for EdgeHandle { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("NodeContainer").finish() + f.debug_struct("EdgeHandle").field("ty", &self.ty).finish_non_exhaustive() } } -impl NodeContainer { - pub fn new(node: TypeErasedBox<'static>) -> SharedNodeContainer { - let node = Box::leak(node); - Self { node }.into() +// SAFETY: wasm is single threaded, so the marker-free payload never actually crosses a thread. +#[cfg(target_family = "wasm")] +unsafe impl Send for EdgeHandle {} +// SAFETY: as in Send. +#[cfg(target_family = "wasm")] +unsafe impl Sync for EdgeHandle {} + +impl EdgeHandle { + pub fn new(node: std::sync::Arc>) -> Self { + Self::new_erased(node, edge_type::()) } - #[cfg(feature = "dealloc_nodes")] - unsafe fn dealloc_unchecked(&mut self) { - unsafe { - drop(Box::from_raw(self.node as *mut TypeErasedNode)); - } + pub fn new_ref(node: std::sync::Arc>) -> Self { + Self::new_erased(node, lend_edge_type::()) } -} -/// Boxes the input and downcasts the output. -/// Wraps around a node taking Box and returning Box -#[derive(Clone)] -pub struct DowncastBothNode { - node: SharedNodeContainer, - _i: PhantomData, - _o: PhantomData, -} -impl<'input, O, I> Node<'input, I> for DowncastBothNode -where - O: 'input + StaticType + WasmNotSend, - I: 'input + StaticType + WasmNotSend, -{ - type Output = DynFuture<'input, 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 - }) + pub fn new_erased(node: std::sync::Arc, ty: Type) -> Self + where + N: ?Sized + 'static + for<'c> Node>, + SharedEdge: WasmNotSend + WasmNotSync, + { + Self { + node: Box::new(SharedEdge::new(node)), + share: |edge| Box::new(edge.downcast_ref::>().expect("share hook matches the stored edge type").share()), + serialize: |edge| Node::::serialize(edge.downcast_ref::>().expect("serialize hook matches the stored edge type")), + ty, } } - fn reset(&self) { - self.node.reset(); - } - fn serialize(&self) -> Option> { - self.node.serialize() + pub fn ty(&self) -> &Type { + &self.ty } -} -impl DowncastBothNode { - pub const fn new(node: SharedNodeContainer) -> Self { + + pub fn duplicate(&self) -> Self { Self { - node, - _i: PhantomData, - _o: PhantomData, + node: (self.share)(&*self.node), + share: self.share, + serialize: self.serialize, + ty: self.ty.clone(), } } -} -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 }) + pub fn serialize(&self) -> Option> { + (self.serialize)(&*self.node) } - #[inline(always)] - fn reset(&self) { - self.node.reset(); + + pub fn downcast(self) -> Result>, ConstructionError> { + self.downcast_erased(edge_type::()) } - #[inline(always)] - fn serialize(&self) -> Option> { - self.node.serialize() + pub fn downcast_lend(self) -> Result>, ConstructionError> { + self.downcast_erased(lend_edge_type::()) } -} -impl FutureWrapperNode { - pub const fn new(node: N) -> Self { - Self { node } + pub fn downcast_erased(self, expected: Type) -> Result, ConstructionError> { + let found = self.ty; + self.node.downcast::>().map(|edge| *edge).map_err(|_| ConstructionError::Type { + expected: Box::new(expected), + found: Box::new(found), + }) } } -pub struct DynAnyNode { - node: Node, - _i: PhantomData, - _o: PhantomData, +pub type NodeConstructor = fn(Vec) -> Result; + +#[derive(Clone)] +pub struct RegistryEntry { + pub io: NodeIOTypes, + pub constructor: NodeConstructor, } -impl<'input, I, O, N> Node<'input, Any<'input>> for DynAnyNode -where - I: 'input + StaticType + WasmNotSend, - O: 'input + StaticType + WasmNotSend, - N: 'input + Node<'input, I, Output = DynFuture<'input, O>>, -{ - type Output = FutureAny<'input>; - #[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}"), +pub fn construct(entry: &RegistryEntry, inputs: Vec) -> Result { + if inputs.len() != entry.io.inputs.len() { + return Err(ConstructionError::Arity { + expected: entry.io.inputs.len(), + got: inputs.len(), + }); + } + for (handle, expected) in inputs.iter().zip(&entry.io.inputs) { + if handle.ty() != expected { + return Err(ConstructionError::Type { + expected: Box::new(expected.clone()), + found: Box::new(handle.ty().clone()), + }); } } + (entry.constructor)(inputs) +} + +#[cfg(not(target_family = "wasm"))] +pub type Any<'n> = Box + 'n + Send>; +#[cfg(target_family = "wasm")] +pub type Any<'n> = Box + 'n>; + +#[cfg(test)] +mod tests { + use super::*; + use crate::SourceId; + use crate::arena::Arena; + use crate::context::{Ctx, EvalScope, ExtractArena}; + use crate::gpoll::GPoll; + use std::sync::Arc; + use std::sync::atomic::{AtomicU32, Ordering}; + + struct CountingNode(AtomicU32); - fn reset(&self) { - self.node.reset(); + impl Node for CountingNode { + type Output = u32; + + fn eval(&self, _input: &Input) -> GPoll { + GPoll::Final(self.0.fetch_add(1, Ordering::Relaxed) + 1) + } } - fn serialize(&self) -> Option> { - self.node.serialize() + struct ValueNode(T); + + impl Node for ValueNode { + type Output = T; + + fn eval(&self, _input: &Input) -> GPoll { + GPoll::Final(self.0.clone()) + } } -} -impl<'input, I, O, N> DynAnyNode -where - I: 'input + StaticType, - O: 'input + StaticType, - N: 'input + Node<'input, I, Output = DynFuture<'input, O>>, -{ - pub const fn new(node: N) -> Self { - Self { - node, - _i: PhantomData, - _o: PhantomData, + + struct LendNode(String); + + impl<'e, Input: Ctx + ExtractArena> Node for LendNode { + type Output = &'e String; + + fn eval(&self, input: &Input) -> GPoll<&'e String> { + match input.arena().alloc(self.0.clone()) { + Some((parked, _)) => GPoll::Final(parked), + None => GPoll::arena_exhausted(), + } } } -} -pub struct PanicNode(PhantomData, PhantomData); -impl<'i, I: 'i + WasmNotSend, O: 'i + WasmNotSend> Node<'i, I> for PanicNode { - type Output = O; - fn eval(&'i self, _: I) -> Self::Output { - unimplemented!("This node should never be evaluated") + fn scope_fixture<'a>(generations: &'a [(SourceId, u64)], arena: &'a Arena) -> EvalScope<'a> { + EvalScope::new(Some(0.5), None, None, generations, arena) } -} -impl PanicNode { - pub const fn new() -> Self { - Self(PhantomData, PhantomData) + #[test] + fn borrow_carrying_value_types_wire_through_the_general_constructor() { + struct SplitBorrow<'c>(&'c str, usize); + + struct SplitNode { + content: Node0, + } + + impl<'e, Input, Node0> Node for SplitNode + where + Input: Ctx, + Node0: Node, + { + type Output = SplitBorrow<'e>; + + fn eval(&self, input: &Input) -> GPoll> { + self.content.eval(input).map(|value| SplitBorrow(value, value.len())) + } + } + + type ErasedSplitEdge = dyn for<'c> Node, Output = SplitBorrow<'c>> + Send + Sync; + + let arena = Arena::new(4096).unwrap(); + let generations = []; + let scope = scope_fixture(&generations, &arena); + let ctx = ContextImpl::root(&scope); + + let lending = EdgeHandle::new_ref(Arc::new(LendNode("held".to_string())) as Arc>); + let upstream = lending.downcast_lend::().unwrap(); + let node: Arc = Arc::new(SplitNode { content: upstream }); + let handle = EdgeHandle::new_erased(node, concrete!(SplitBorrow<'static>)); + assert_eq!(*handle.ty(), concrete!(SplitBorrow<'static>)); + + let wired = handle.downcast_erased::(concrete!(SplitBorrow<'static>)).unwrap(); + let GPoll::Final(split) = wired.eval(&ctx) else { + panic!("borrow-carrying output must eval through the erased edge"); + }; + assert_eq!(split.0, "held"); + assert_eq!(split.1, 4); } -} -impl Default for PanicNode { - fn default() -> Self { - Self::new() + #[test] + fn derive_ctx_repeat_pushes_index_levels_through_the_erased_edge() { + use crate::context::{DeriveCtx, Derived, ExtractIndex}; + + struct RepeatNode { + content: Node0, + } + + impl Node for RepeatNode + where + C: Ctx + DeriveCtx, + Node0: for<'x> Node, Output = T>, + { + type Output = Vec; + + fn eval(&self, input: &C) -> GPoll> { + let spilled = input.index_head(); + let mut result = Vec::new(); + for index in 0..3 { + let derived = input.promoted(&spilled, index); + match self.content.eval(&derived) { + GPoll::Final(value) => result.push(value), + other => return other.map(|_| Vec::new()), + } + } + GPoll::Final(result) + } + } + + struct LevelsNode; + + impl Node for LevelsNode { + type Output = Vec; + + fn eval(&self, input: &Input) -> GPoll> { + GPoll::Final(input.try_index().map(|levels| levels.collect()).unwrap_or_default()) + } + } + + let arena = Arena::new(1024).unwrap(); + let generations = []; + let scope = scope_fixture(&generations, &arena); + let ctx = ContextImpl::root(&scope); + + let nested = RepeatNode { + content: RepeatNode { content: LevelsNode }, + }; + let erased: Box>>>> = Box::new(nested); + + let GPoll::Final(outer) = erased.eval(&ctx) else { + panic!("nested repeat must evaluate"); + }; + assert_eq!(outer.len(), 3); + assert_eq!(outer[2][1], vec![1, 2, 0]); + assert_eq!(outer[0][0], vec![0, 0, 0]); } -} -// TODO: Evaluate safety -unsafe impl Sync for PanicNode {} + #[test] + fn derive_ctx_footprint_replace_reaches_the_content() { + use crate::context::{DeriveCtx, Derived, ExtractFootprint}; + use crate::transform::Footprint; + + struct ShiftFootprintNode { + content: Node0, + } + + impl Node for ShiftFootprintNode + where + C: Ctx + DeriveCtx + ExtractFootprint, + Node0: for<'x> Node, Output = T>, + { + type Output = T; + + fn eval(&self, input: &C) -> GPoll { + let mut footprint = input.try_footprint().copied().unwrap_or(Footprint::DEFAULT); + footprint.resolution.x += 7; + let derived = input.with_footprint(&footprint); + self.content.eval(&derived) + } + } + + struct ResolutionNode; + + impl Node for ResolutionNode { + type Output = u32; + + fn eval(&self, input: &Input) -> GPoll { + GPoll::Final(input.try_footprint().map(|footprint| footprint.resolution.x).unwrap_or(0)) + } + } + + let arena = Arena::new(1024).unwrap(); + let generations = []; + let scope = scope_fixture(&generations, &arena); + let ctx = ContextImpl::root(&scope); + + let graph: Box> = Box::new(ShiftFootprintNode { + content: ShiftFootprintNode { content: ResolutionNode }, + }); + assert_eq!(graph.eval(&ctx), GPoll::Final(Footprint::DEFAULT.resolution.x + 14)); + } + + #[test] + fn construct_checks_arity_and_types() { + fn construct_strlen(args: Vec) -> Result { + let mut args = args.into_iter(); + let value = args.next().ok_or(ConstructionError::Arity { expected: 1, got: 0 })?.downcast::()?; + drop(value); + Ok(EdgeHandle::new(Arc::new(ValueNode(0u32)) as Arc>)) + } + let entry = RegistryEntry { + io: NodeIOTypes::new(concrete!(Context), concrete!(u32), vec![edge_type::()]), + constructor: construct_strlen, + }; + + let owned = EdgeHandle::new(Arc::new(ValueNode("typed".to_string())) as Arc>); + assert!(construct(&entry, vec![owned]).is_ok()); + + assert_eq!(construct(&entry, vec![]).unwrap_err(), ConstructionError::Arity { expected: 1, got: 0 }); + + let mistyped = EdgeHandle::new(Arc::new(ValueNode(1.0f64)) as Arc>); + assert_eq!( + construct(&entry, vec![mistyped]).unwrap_err(), + ConstructionError::Type { + expected: Box::new(edge_type::()), + found: Box::new(edge_type::()), + } + ); + + let lent = EdgeHandle::new_ref(Arc::new(LendNode("typed".to_string())) as Arc>); + assert_eq!( + construct(&entry, vec![lent]).unwrap_err(), + ConstructionError::Type { + expected: Box::new(edge_type::()), + found: Box::new(lend_edge_type::()), + } + ); + } + + #[test] + fn duplicated_edges_share_one_instance_and_outlive_each_other() { + let arena = Arena::new(1024).unwrap(); + let generations = []; + let scope = scope_fixture(&generations, &arena); + let ctx = ContextImpl::root(&scope); + + let handle = EdgeHandle::new(Arc::new(CountingNode(AtomicU32::new(0))) as Arc>); + let duplicate = handle.duplicate(); + assert_eq!(*duplicate.ty(), edge_type::()); + + let first = handle.downcast::().unwrap(); + let second = duplicate.downcast::().unwrap(); + assert_eq!(first.eval(&ctx), GPoll::Final(1)); + assert_eq!(second.eval(&ctx), GPoll::Final(2)); + + drop(first); + assert_eq!(second.eval(&ctx), GPoll::Final(3)); + } +} 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 0000000000..2b64275d23 --- /dev/null +++ b/node-graph/libraries/core-types/src/runtime.rs @@ -0,0 +1,586 @@ +use crate::SourceId; +use std::collections::HashMap; +use std::future::Future; +use std::pin::Pin; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex, PoisonError}; + +#[cfg(not(target_family = "wasm"))] +pub type SourceFuture = Pin + Send + 'static>>; +#[cfg(target_family = "wasm")] +pub type SourceFuture = Pin + 'static>>; + +#[cfg(not(target_family = "wasm"))] +pub type DynRuntime = dyn Runtime + Send + Sync; +#[cfg(target_family = "wasm")] +pub type DynRuntime = dyn Runtime; + +pub trait Runtime { + /// Returns true when the future completed during the call, so its result is already observable. + fn spawn(&self, source: SourceId, future: SourceFuture) -> bool; +} + +#[derive(Clone)] +pub struct RuntimeHandle(pub Arc); + +// SAFETY: wasm is single threaded, so the handle never actually crosses a thread. +#[cfg(target_family = "wasm")] +unsafe impl Send for RuntimeHandle {} +// SAFETY: as in Send. +#[cfg(target_family = "wasm")] +unsafe impl Sync for RuntimeHandle {} + +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 graphene_hash::CacheHash for RuntimeHandle { + fn cache_hash(&self, _state: &mut H) {} +} + +pub trait Spawner { + /// Returns true when the task completed during the call, so its result is already observable. + fn spawn(&self, task: SourceFuture) -> bool; +} + +/// Polls `task` once with a no-op waker, returning true if it completed. +pub fn poll_once(task: &mut SourceFuture) -> bool { + let mut context = std::task::Context::from_waker(std::task::Waker::noop()); + task.as_mut().poll(&mut context).is_ready() +} + +#[cfg(not(target_family = "wasm"))] +pub type DynSpawner = dyn Spawner + Send + Sync; +#[cfg(target_family = "wasm")] +pub type DynSpawner = dyn Spawner; + +#[cfg(not(target_family = "wasm"))] +pub type DynNotifier = dyn Fn() + Send + Sync; +#[cfg(target_family = "wasm")] +pub type DynNotifier = dyn Fn(); + +impl Spawner for Box { + fn spawn(&self, task: SourceFuture) -> bool { + (**self).spawn(task) + } +} + +/// Polls each task once inline. Tasks that are not immediately ready never complete. +pub struct NoopSpawner; + +impl Spawner for NoopSpawner { + fn spawn(&self, mut task: SourceFuture) -> bool { + if poll_once(&mut task) { + return true; + } + log::warn!("async source is not immediately ready and no host spawner is wired; the task is dropped"); + false + } +} + +pub type DynGraphRuntime = GraphRuntime>; + +impl Default for RuntimeHandle { + fn default() -> Self { + Self(Arc::new(GraphRuntime::new(Box::new(NoopSpawner) as Box))) + } +} + +pub struct GraphRuntime { + generations: Arc>>, + dirty: Arc, + notifier: Arc>>, + spawner: S, +} + +// SAFETY: wasm is single threaded, so the runtime never actually crosses a thread. +#[cfg(target_family = "wasm")] +unsafe impl Send for GraphRuntime {} +// SAFETY: as in Send. +#[cfg(target_family = "wasm")] +unsafe impl Sync for GraphRuntime {} + +impl GraphRuntime { + pub fn new(spawner: S) -> Self { + Self { + generations: Arc::default(), + dirty: Arc::default(), + notifier: Arc::new(Mutex::new(Arc::new(|| {}))), + spawner, + } + } + + pub fn set_notifier(&self, notifier: Arc) { + *self.notifier.lock().unwrap_or_else(PoisonError::into_inner) = notifier; + } + + pub fn retain_sources(&self, live: &[SourceId]) { + let mut generations = self.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) -> Vec<(SourceId, u64)> { + let generations = self.generations.lock().unwrap_or_else(PoisonError::into_inner); + let mut snapshot: Vec<_> = generations.iter().map(|(source, generation)| (*source, *generation)).collect(); + snapshot.sort_unstable(); + snapshot + } + + pub fn take_dirty(&self) -> bool { + self.dirty.swap(false, Ordering::Acquire) + } + + pub fn spawner(&self) -> &S { + &self.spawner + } +} + +impl Runtime for GraphRuntime { + fn spawn(&self, source: SourceId, mut future: SourceFuture) -> bool { + let generations = Arc::clone(&self.generations); + let dirty = Arc::clone(&self.dirty); + let notifier = Arc::clone(&self.notifier); + let mut first = true; + self.spawner.spawn(Box::pin(std::future::poll_fn(move |task_context| { + let poll = future.as_mut().poll(task_context); + if poll.is_ready() && !first { + let mut generations = generations.lock().unwrap_or_else(PoisonError::into_inner); + if let Some(generation) = generations.get_mut(&source) { + *generation += 1; + dirty.store(true, Ordering::Release); + drop(generations); + let notifier = Arc::clone(¬ifier.lock().unwrap_or_else(PoisonError::into_inner)); + notifier(); + } + } + first = false; + poll + }))) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::arena::Arena; + use crate::context::{ContextImpl, Ctx, CtxSnapshot, EvalScope, ExtractFootprint, ExtractVarArgs, VarArgLink, VarArgSlots}; + use crate::gpoll::GPoll; + use crate::node::Node; + use crate::transform::Footprint; + use std::sync::Mutex; + use std::sync::atomic::{AtomicU32, Ordering}; + + #[derive(Default)] + struct MockRuntime { + futures: Mutex>, + } + + impl Runtime for MockRuntime { + fn spawn(&self, source: SourceId, future: SourceFuture) -> bool { + self.futures.lock().unwrap().push((source, future)); + false + } + } + + impl MockRuntime { + fn drain(&self) -> Vec { + let futures = std::mem::take(&mut *self.futures.lock().unwrap()); + let mut task_ctx = std::task::Context::from_waker(std::task::Waker::noop()); + futures + .into_iter() + .map(|(source, mut future)| { + assert!(future.as_mut().poll(&mut task_ctx).is_ready()); + source + }) + .collect() + } + } + + #[derive(Default)] + struct CollectSpawner { + tasks: Mutex>, + } + + impl Spawner for CollectSpawner { + fn spawn(&self, mut task: SourceFuture) -> bool { + if poll_once(&mut task) { + return true; + } + self.tasks.lock().unwrap().push(task); + false + } + } + + struct YieldOnce(bool); + + impl Future for YieldOnce { + type Output = (); + + fn poll(mut self: Pin<&mut Self>, task_context: &mut std::task::Context<'_>) -> std::task::Poll<()> { + if self.0 { + return std::task::Poll::Ready(()); + } + self.0 = true; + task_context.waker().wake_by_ref(); + std::task::Poll::Pending + } + } + + fn yield_once() -> YieldOnce { + YieldOnce(false) + } + + impl CollectSpawner { + fn drain(&self) -> usize { + let tasks = std::mem::take(&mut *self.tasks.lock().unwrap()); + let mut task_ctx = std::task::Context::from_waker(std::task::Waker::noop()); + let count = tasks.len(); + for mut task in tasks { + assert!(task.as_mut().poll(&mut task_ctx).is_ready()); + } + count + } + } + + struct SourceNode(T); + + impl Node for SourceNode { + type Output = T; + + fn eval(&self, _input: &Input) -> GPoll { + GPoll::Final(self.0.clone()) + } + } + + static SLOW_DOUBLE_RUNS: AtomicU32 = AtomicU32::new(0); + + #[node_macro::node(category(""))] + async fn slow_double(_: impl Ctx, value: f64) -> f64 { + SLOW_DOUBLE_RUNS.fetch_add(1, Ordering::Relaxed); + value * 2. + } + + fn stand_in(_value: &f64) -> f64 { + -1. + } + + #[node_macro::node(category(""), placeholder(stand_in))] + async fn preview_double(_: impl Ctx, value: f64) -> f64 { + value * 2. + } + + #[node_macro::node(category(""), placeholder(stand_in), no_partial)] + async fn strict_double(_: impl Ctx, value: f64) -> f64 { + value * 2. + } + + #[node_macro::node(category(""))] + async fn snapshot_resolution(ctx: CtxSnapshot, _primary: ()) -> u32 { + ctx.try_footprint().map(|footprint| footprint.resolution.x).unwrap_or(0) + } + + #[node_macro::node(category(""))] + async fn snapshot_vararg(ctx: CtxSnapshot, _primary: ()) -> f64 { + ctx.vararg(0).ok().and_then(|slot| slot.downcast_ref::()).copied().unwrap_or(0.) + } + + static STAGED_RUNS: AtomicU32 = AtomicU32::new(0); + + #[node_macro::node(category(""))] + fn staged_double(_: impl Ctx, value: f64) -> SourceFuture { + STAGED_RUNS.fetch_add(1, Ordering::Relaxed); + Box::pin(async move { value * 2. }) + } + + #[node_macro::node(category(""))] + fn staged_sum(ctx: impl Ctx, value: f64, addend: impl Node, Output = f64>) -> Result, crate::gpoll::Interrupt> { + let addend = addend.eval(ctx)?; + Ok(Box::pin(async move { value + addend })) + } + + struct GatedSource(Arc, f64); + + impl Node for GatedSource { + type Output = f64; + + fn eval(&self, _input: &Input) -> GPoll { + match self.0.load(Ordering::Relaxed) { + true => GPoll::Final(self.1), + false => GPoll::Pending, + } + } + } + + fn scope_fixture<'a>(generations: &'a [(SourceId, u64)], arena: &'a Arena) -> EvalScope<'a> { + EvalScope::new(None, None, None, generations, arena) + } + + #[test] + fn async_source_spawns_once_and_lands_via_the_slot() { + let arena = Arena::new(64).unwrap(); + let generations = []; + let scope = scope_fixture(&generations, &arena); + let ctx = ContextImpl::root(&scope); + + let runtime = Arc::new(MockRuntime::default()); + let graph = SlowDoubleNode::new(SourceNode(21.0f64), SourceNode(RuntimeHandle(runtime.clone())), SourceNode(7u64)); + + assert_eq!(Node::eval(&graph, &ctx), GPoll::Pending); + assert_eq!(Node::eval(&graph, &ctx), GPoll::Pending); + assert_eq!(SLOW_DOUBLE_RUNS.load(Ordering::Relaxed), 0); + assert_eq!(runtime.drain(), vec![7]); + assert_eq!(SLOW_DOUBLE_RUNS.load(Ordering::Relaxed), 1); + assert_eq!(Node::eval(&graph, &ctx), GPoll::Final(42.0)); + assert_eq!(Node::eval(&graph, &ctx), GPoll::Final(42.0)); + assert_eq!(SLOW_DOUBLE_RUNS.load(Ordering::Relaxed), 1); + } + + #[test] + fn async_source_reports_the_placeholder_while_in_flight() { + let arena = Arena::new(64).unwrap(); + let generations = []; + let scope = scope_fixture(&generations, &arena); + let ctx = ContextImpl::root(&scope); + + let runtime = Arc::new(MockRuntime::default()); + let graph = PreviewDoubleNode::new(SourceNode(21.0f64), SourceNode(RuntimeHandle(runtime.clone())), SourceNode(1u64)); + + assert_eq!(Node::eval(&graph, &ctx), GPoll::Partial(-1.0)); + runtime.drain(); + assert_eq!(Node::eval(&graph, &ctx), GPoll::Final(42.0)); + } + + #[test] + fn no_partial_maps_the_placeholder_frame_to_pending() { + let arena = Arena::new(64).unwrap(); + let generations = []; + let scope = scope_fixture(&generations, &arena); + let ctx = ContextImpl::root(&scope); + + let runtime = Arc::new(MockRuntime::default()); + let graph = StrictDoubleNode::new(SourceNode(21.0f64), SourceNode(RuntimeHandle(runtime.clone())), SourceNode(2u64)); + + assert_eq!(Node::eval(&graph, &ctx), GPoll::Pending); + runtime.drain(); + assert_eq!(Node::eval(&graph, &ctx), GPoll::Final(42.0)); + } + + #[test] + fn prologue_runs_sync_and_spawns_once() { + let arena = Arena::new(64).unwrap(); + let generations = []; + let scope = scope_fixture(&generations, &arena); + let ctx = ContextImpl::root(&scope); + + let runtime = Arc::new(MockRuntime::default()); + let graph = StagedDoubleNode::new(SourceNode(21.0f64), SourceNode(RuntimeHandle(runtime.clone())), SourceNode(8u64)); + + assert_eq!(Node::eval(&graph, &ctx), GPoll::Pending); + assert_eq!(STAGED_RUNS.load(Ordering::Relaxed), 1, "the prologue runs synchronously on the miss"); + assert_eq!(Node::eval(&graph, &ctx), GPoll::Pending); + assert_eq!(STAGED_RUNS.load(Ordering::Relaxed), 1, "in flight must not rerun the prologue"); + assert_eq!(runtime.drain(), vec![8]); + assert_eq!(Node::eval(&graph, &ctx), GPoll::Final(42.0)); + assert_eq!(STAGED_RUNS.load(Ordering::Relaxed), 1); + } + + #[test] + fn prologue_interrupt_defers_the_spawn() { + let arena = Arena::new(64).unwrap(); + let generations = []; + let scope = scope_fixture(&generations, &arena); + let ctx = ContextImpl::root(&scope); + + let gate = Arc::new(std::sync::atomic::AtomicBool::new(false)); + let runtime = Arc::new(MockRuntime::default()); + let graph = StagedSumNode::new(SourceNode(40.0f64), GatedSource(gate.clone(), 2.0), SourceNode(RuntimeHandle(runtime.clone())), SourceNode(9u64)); + + assert_eq!(Node::eval(&graph, &ctx), GPoll::Pending); + assert_eq!(runtime.drain(), Vec::::new(), "an interrupted prologue must not spawn or claim the slot"); + gate.store(true, Ordering::Relaxed); + assert_eq!(Node::eval(&graph, &ctx), GPoll::Pending); + assert_eq!(runtime.drain(), vec![9]); + assert_eq!(Node::eval(&graph, &ctx), GPoll::Final(42.0)); + } + + #[test] + fn async_kernels_read_captured_varargs() { + let arena = Arena::new(64).unwrap(); + let generations = []; + let scope = scope_fixture(&generations, &arena); + let root = ContextImpl::root(&scope); + let payload = 21.5f64; + let link = VarArgLink { + args: VarArgSlots::Single(&payload), + outer: None, + }; + let ctx = root.with_varargs(&link); + + let runtime = Arc::new(MockRuntime::default()); + let graph = SnapshotVarargNode::new(SourceNode(()), SourceNode(RuntimeHandle(runtime.clone())), SourceNode(5u64)); + + assert_eq!(Node::eval(&graph, &ctx), GPoll::Pending); + runtime.drain(); + assert_eq!(Node::eval(&graph, &ctx), GPoll::Final(21.5)); + } + + #[test] + fn async_kernels_read_the_captured_context_snapshot() { + let arena = Arena::new(64).unwrap(); + let generations = []; + let scope = scope_fixture(&generations, &arena); + let root = ContextImpl::root(&scope); + let footprint = Footprint::DEFAULT; + let ctx = root.with_footprint(&footprint); + + let runtime = Arc::new(MockRuntime::default()); + let graph = SnapshotResolutionNode::new(SourceNode(()), SourceNode(RuntimeHandle(runtime.clone())), SourceNode(3u64)); + + assert_eq!(Node::eval(&graph, &ctx), GPoll::Pending); + runtime.drain(); + assert_eq!(Node::eval(&graph, &ctx), GPoll::Final(Footprint::DEFAULT.resolution.x)); + } + + #[test] + fn the_epilogue_bumps_the_generation_and_sets_dirty() { + let runtime = GraphRuntime::new(CollectSpawner::default()); + runtime.retain_sources(&[7]); + + Runtime::spawn(&runtime, 7, Box::pin(yield_once())); + assert_eq!(runtime.snapshot(), vec![(7, 0)], "no bump before the future completes"); + assert!(!runtime.take_dirty()); + + assert_eq!(runtime.spawner().drain(), 1); + assert_eq!(runtime.snapshot(), vec![(7, 1)]); + assert!(runtime.take_dirty()); + assert!(!runtime.take_dirty(), "take_dirty drains the flag"); + } + + #[test] + fn the_epilogue_notifies_after_setting_dirty() { + let runtime = GraphRuntime::new(CollectSpawner::default()); + runtime.retain_sources(&[7]); + let observed_dirty = Arc::new(AtomicBool::new(false)); + let dirty_at_notify = Arc::clone(&runtime.dirty); + let observed = Arc::clone(&observed_dirty); + runtime.set_notifier(Arc::new(move || { + observed.store(dirty_at_notify.load(Ordering::Acquire), Ordering::Relaxed); + })); + + Runtime::spawn(&runtime, 7, Box::pin(yield_once())); + assert_eq!(runtime.spawner().drain(), 1); + assert!(observed_dirty.load(Ordering::Relaxed), "the notifier must observe the dirty flag already set"); + } + + #[test] + fn the_epilogue_of_a_removed_source_does_not_notify() { + let runtime = GraphRuntime::new(CollectSpawner::default()); + runtime.retain_sources(&[7]); + let notified = Arc::new(AtomicBool::new(false)); + let flag = Arc::clone(¬ified); + runtime.set_notifier(Arc::new(move || flag.store(true, Ordering::Relaxed))); + + Runtime::spawn(&runtime, 7, Box::pin(yield_once())); + runtime.retain_sources(&[]); + assert_eq!(runtime.spawner().drain(), 1); + assert!(!notified.load(Ordering::Relaxed)); + } + + #[test] + fn the_epilogue_of_a_removed_source_is_inert() { + let runtime = GraphRuntime::new(CollectSpawner::default()); + runtime.retain_sources(&[7]); + + Runtime::spawn(&runtime, 7, Box::pin(yield_once())); + runtime.retain_sources(&[]); + assert_eq!(runtime.spawner().drain(), 1); + + assert_eq!(runtime.snapshot(), Vec::<(SourceId, u64)>::new()); + assert!(!runtime.take_dirty(), "a removed source must not invalidate"); + } + + #[test] + fn retain_sources_preserves_live_generations() { + let runtime = GraphRuntime::new(CollectSpawner::default()); + runtime.retain_sources(&[7]); + Runtime::spawn(&runtime, 7, Box::pin(yield_once())); + runtime.spawner().drain(); + + runtime.retain_sources(&[7, 9]); + assert_eq!(runtime.snapshot(), vec![(7, 1), (9, 0)]); + + runtime.retain_sources(&[9]); + assert_eq!(runtime.snapshot(), vec![(9, 0)]); + } + + #[node_macro::node(category(""))] + async fn epilogue_double(_: impl Ctx, value: f64) -> f64 { + yield_once().await; + value * 2. + } + + #[node_macro::node(category(""))] + async fn inline_double(_: impl Ctx, value: f64) -> f64 { + value * 2. + } + + #[test] + fn an_immediately_ready_task_completes_inline_without_invalidating() { + let runtime = GraphRuntime::new(CollectSpawner::default()); + runtime.retain_sources(&[7]); + + assert!(Runtime::spawn(&runtime, 7, Box::pin(async {}))); + assert_eq!(runtime.spawner().drain(), 0); + assert_eq!(runtime.snapshot(), vec![(7, 0)], "inline completion must not bump the generation"); + assert!(!runtime.take_dirty()); + } + + #[test] + fn an_immediately_ready_kernel_returns_final_on_the_first_eval() { + let arena = Arena::new(64).unwrap(); + let runtime = Arc::new(GraphRuntime::new(CollectSpawner::default())); + runtime.retain_sources(&[13]); + let graph = InlineDoubleNode::new(SourceNode(21.0f64), SourceNode(RuntimeHandle(runtime.clone())), SourceNode(13u64)); + + let snapshot = runtime.snapshot(); + let scope = EvalScope::new(None, None, None, &snapshot, &arena); + let ctx = ContextImpl::root(&scope); + assert_eq!(Node::eval(&graph, &ctx), GPoll::Final(42.0)); + assert!(!runtime.take_dirty()); + assert_eq!(runtime.snapshot(), vec![(13, 0)]); + assert_eq!(runtime.spawner().drain(), 0); + } + + #[test] + fn a_source_slot_lands_through_the_runtime_while_downstream_keys_invalidate() { + let arena = Arena::new(64).unwrap(); + let runtime = Arc::new(GraphRuntime::new(CollectSpawner::default())); + runtime.retain_sources(&[11]); + let graph = EpilogueDoubleNode::new(SourceNode(21.0f64), SourceNode(RuntimeHandle(runtime.clone())), SourceNode(11u64)); + + let snapshot = runtime.snapshot(); + let scope = EvalScope::new(None, None, None, &snapshot, &arena); + let ctx = ContextImpl::root(&scope); + assert_eq!(Node::eval(&graph, &ctx), GPoll::Pending); + assert!(!runtime.take_dirty()); + + assert_eq!(runtime.spawner().drain(), 1); + assert!(runtime.take_dirty()); + let bumped = runtime.snapshot(); + assert_eq!(bumped, vec![(11, 1)]); + + let bumped_scope = EvalScope::new(None, None, None, &bumped, &arena); + let bumped_ctx = ContextImpl::root(&bumped_scope); + assert_eq!(Node::eval(&graph, &bumped_ctx), GPoll::Final(42.0), "the own-generation-excluded key replays the landed slot"); + assert_eq!(runtime.spawner().drain(), 0, "a slot hit must not respawn"); + + let downstream_key = crate::registry::cache_key(&ContextImpl::root(&scope)); + let bumped_downstream_key = crate::registry::cache_key(&ContextImpl::root(&bumped_scope)); + assert_ne!(downstream_key, bumped_downstream_key, "unretained keys see the bump"); + } +} diff --git a/node-graph/libraries/core-types/src/types.rs b/node-graph/libraries/core-types/src/types.rs index 282d470ef1..97644818aa 100644 --- a/node-graph/libraries/core-types/src/types.rs +++ b/node-graph/libraries/core-types/src/types.rs @@ -235,6 +235,7 @@ pub enum Type { Fn(Box, Box), /// Represents a future which promises to return the inner type. Future(Box), + Ref(Box), } impl Default for Type { @@ -308,6 +309,7 @@ impl Type { Self::Concrete(ty) => Some(ty.size), Self::Fn(_, _) => None, Self::Future(_) => None, + Self::Ref(_) => None, } } @@ -317,6 +319,7 @@ impl Type { Self::Concrete(ty) => Some(ty.align), Self::Fn(_, _) => None, Self::Future(_) => None, + Self::Ref(_) => None, } } @@ -326,6 +329,7 @@ impl Type { Self::Concrete(_) => self, Self::Fn(_, output) => output.nested_type(), Self::Future(output) => output.nested_type(), + Self::Ref(inner) => inner.nested_type(), } } @@ -338,6 +342,7 @@ impl Type { Self::Concrete(_) => None, Self::Fn(_, output) => output.replace_nested(f), Self::Future(output) => output.replace_nested(f), + Self::Ref(inner) => inner.replace_nested(f), } } @@ -347,6 +352,7 @@ impl Type { 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::Ref(ty) => ty.identifier_name(), } } } @@ -361,7 +367,7 @@ pub fn simplify_identifier_name(ty: &str) -> String { /// Converts a Rust-internal type name to its user-facing form. pub fn make_type_user_readable(ty: &str) -> String { let ty = ty - .replace("Option>", "Context") + .replace("ContextImpl", "Context") .replace("Raster", "Raster") .replace("Raster", "Raster") .replace("DAffine2", "Transform") @@ -441,6 +447,7 @@ impl std::fmt::Display for Type { Type::Concrete(ty) => write!(f, "{ty}"), Type::Fn(_, return_value) => write!(f, "{return_value}"), Type::Future(ty) => write!(f, "{ty}"), + Type::Ref(ty) => write!(f, "{ty}"), } } } diff --git a/node-graph/libraries/core-types/src/value.rs b/node-graph/libraries/core-types/src/value.rs index 3cf1c0f6a6..7fc7852415 100644 --- a/node-graph/libraries/core-types/src/value.rs +++ b/node-graph/libraries/core-types/src/value.rs @@ -1,101 +1,16 @@ -use crate::Node; -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); +#[derive(Clone, Copy)] +pub struct ClonedNode(pub T); -impl<'i, T: Default + 'i, I> Node<'i, I> for OnceCellNode { +impl crate::node::Node for ClonedNode { 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)) + fn eval(&self, _input: &Input) -> crate::gpoll::GPoll { + crate::gpoll::GPoll::Final(self.0.clone()) } } -#[derive(Clone, Copy)] -pub struct ClonedNode(pub T); - -impl<'i, T: Clone + 'i, I> Node<'i, I> for ClonedNode { - type Output = T; - #[inline(always)] - fn eval(&'i self, _input: I) -> Self::Output { - self.0.clone() - } +pub fn value_edge(value: T) -> crate::registry::EdgeHandle { + crate::registry::EdgeHandle::new(std::sync::Arc::new(ClonedNode(value)) as std::sync::Arc>) } impl ClonedNode { @@ -109,103 +24,3 @@ impl From for ClonedNode { ClonedNode::new(value) } } - -#[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 8b2c6ae6b5..730fa5ec9d 100644 --- a/node-graph/libraries/graphene-hash/src/lib.rs +++ b/node-graph/libraries/graphene-hash/src/lib.rs @@ -68,6 +68,7 @@ impl_via_hash! { #[cfg(feature = "std")] impl_via_hash! { String, + core::time::Duration, } impl<'a> CacheHash for std::borrow::Cow<'a, str> { @@ -235,3 +236,142 @@ impl_tuple!(A, B, C); impl_tuple!(A, B, C, D); impl_tuple!(A, B, C, D, E); impl_tuple!(A, B, C, D, E, F); + +/// rustc-hash's polynomial hash with the state pinned to u64, so keys match across native and wasm targets. +/// The state starts at a nonzero seed: zero-initialized fx absorbs leading zero words, which produced +/// a real wrong-value memo hit in the prototype. +#[derive(Clone)] +pub struct FxHasher64 { + hash: u64, +} + +const K: u64 = 0xf1357aea2e62a9c5; +const SEED: u64 = 0x517cc1b727220a95; +const SEED1: u64 = 0x243f6a8885a308d3; +const SEED2: u64 = 0x13198a2e03707344; +const PREVENT_TRIVIAL_ZERO_COLLAPSE: u64 = 0xa4093822299f31d0; + +impl Default for FxHasher64 { + fn default() -> Self { + Self::new() + } +} + +impl FxHasher64 { + pub const fn new() -> Self { + Self { hash: SEED } + } + + #[inline] + fn add_to_hash(&mut self, i: u64) { + self.hash = self.hash.wrapping_add(i).wrapping_mul(K); + } +} + +impl core::hash::Hasher for FxHasher64 { + #[inline] + fn write(&mut self, bytes: &[u8]) { + self.add_to_hash(hash_bytes(bytes)); + } + + #[inline] + fn write_u8(&mut self, i: u8) { + self.add_to_hash(i as u64); + } + + #[inline] + fn write_u16(&mut self, i: u16) { + self.add_to_hash(i as u64); + } + + #[inline] + fn write_u32(&mut self, i: u32) { + self.add_to_hash(i as u64); + } + + #[inline] + fn write_u64(&mut self, i: u64) { + self.add_to_hash(i); + } + + #[inline] + fn write_u128(&mut self, i: u128) { + self.add_to_hash(i as u64); + self.add_to_hash((i >> 64) as u64); + } + + #[inline] + fn write_usize(&mut self, i: usize) { + self.add_to_hash(i as u64); + } + + #[inline] + fn finish(&self) -> u64 { + self.hash.rotate_left(26) + } +} + +#[inline] +fn multiply_mix(x: u64, y: u64) -> u64 { + let full = (x as u128) * (y as u128); + (full as u64) ^ ((full >> 64) as u64) +} + +#[inline] +fn hash_bytes(bytes: &[u8]) -> u64 { + let len = bytes.len(); + let mut s0 = SEED1; + let mut s1 = SEED2; + + if len <= 16 { + if len >= 8 { + s0 ^= u64::from_le_bytes(bytes[0..8].try_into().unwrap()); + s1 ^= u64::from_le_bytes(bytes[len - 8..].try_into().unwrap()); + } else if len >= 4 { + s0 ^= u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as u64; + s1 ^= u32::from_le_bytes(bytes[len - 4..].try_into().unwrap()) as u64; + } else if len > 0 { + let lo = bytes[0]; + let mid = bytes[len / 2]; + let hi = bytes[len - 1]; + s0 ^= lo as u64; + s1 ^= ((hi as u64) << 8) | mid as u64; + } + } else { + let mut off = 0; + while off < len - 16 { + let x = u64::from_le_bytes(bytes[off..off + 8].try_into().unwrap()); + let y = u64::from_le_bytes(bytes[off + 8..off + 16].try_into().unwrap()); + let t = multiply_mix(s0 ^ x, PREVENT_TRIVIAL_ZERO_COLLAPSE ^ y); + s0 = s1; + s1 = t; + off += 16; + } + + let suffix = &bytes[len - 16..]; + s0 ^= u64::from_le_bytes(suffix[0..8].try_into().unwrap()); + s1 ^= u64::from_le_bytes(suffix[8..16].try_into().unwrap()); + } + + multiply_mix(s0, s1) ^ (len as u64) +} + +#[cfg(test)] +mod tests { + use super::FxHasher64; + use core::hash::Hasher; + + #[test] + fn leading_zero_words_are_not_absorbed() { + let hash_words = |words: &[u64]| { + let mut hasher = FxHasher64::new(); + for &word in words { + hasher.write_u64(word); + } + hasher.finish() + }; + assert_ne!(hash_words(&[]), hash_words(&[0]), "a zero word must change the hash of the empty input"); + assert_ne!(hash_words(&[0]), hash_words(&[0, 0]), "zero words must accumulate distinct states"); + assert_ne!(hash_words(&[0, 7]), hash_words(&[7]), "a leading zero word must not be absorbed"); + } +} diff --git a/node-graph/libraries/wgpu-executor/src/lib.rs b/node-graph/libraries/wgpu-executor/src/lib.rs index 4c724ec684..12d0095f32 100644 --- a/node-graph/libraries/wgpu-executor/src/lib.rs +++ b/node-graph/libraries/wgpu-executor/src/lib.rs @@ -9,17 +9,16 @@ 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 raster_types::Texture; use std::sync::Arc; +use std::sync::Mutex; use vello::{AaConfig, AaSupport, RenderParams, Renderer, RendererOptions, Scene}; use wgpu::{Origin3d, TextureAspect}; 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 rendering::RenderContext; @@ -61,6 +60,18 @@ impl std::fmt::Debug for WgpuExecutor { } } +/// Owned Arc handle carrying the executor as an ordinary wire value. +#[derive(Clone, Debug)] +pub struct WgpuExecutorHandle(pub std::sync::Arc); + +impl std::ops::Deref for WgpuExecutorHandle { + type Target = WgpuExecutor; + + fn deref(&self) -> &WgpuExecutor { + &self.0 + } +} + impl<'a, T: ApplicationIo> From<&'a EditorApi> for &'a WgpuExecutor { fn from(editor_api: &'a EditorApi) -> Self { editor_api.application_io.as_ref().unwrap().gpu_executor().unwrap() @@ -68,8 +79,8 @@ 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 { - let texture = self.request_texture(size).await; + 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()); @@ -82,7 +93,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(), @@ -109,8 +120,8 @@ impl WgpuExecutor { pipeline.init::

(self); } - pub async fn request_texture(&self, size: UVec2) -> Texture { - self.inner.texture_cache.lock().await.request_texture(&self.context().device, size) + pub fn request_texture(&self, size: UVec2) -> Texture { + self.inner.texture_cache.lock().unwrap().request_texture(&self.context().device, size) } } diff --git a/node-graph/libraries/wgpu-executor/src/pipeline.rs b/node-graph/libraries/wgpu-executor/src/pipeline.rs index 75d286d834..2b79447ed7 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<'a>(&'a self, executor: &'a WgpuExecutor, args: &'a 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) + 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 9f393d6945..ef62a9de46 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,10 +2,10 @@ use crate::WgpuContext; use crate::shader_runtime::{FULLSCREEN_VERTEX_SHADER_NAME, ShaderRuntime}; use core_types::list::{Item, List}; use core_types::shaders::buffer_struct::BufferStruct; -use futures::lock::Mutex; use raster_types::{GPU, Raster}; use std::borrow::Cow; use std::collections::HashMap; +use std::sync::{Mutex, PoisonError}; use wgpu::util::{BufferInitDescriptor, DeviceExt}; use wgpu::{ BindGroupDescriptor, BindGroupEntry, BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingResource, BindingType, Buffer, BufferBinding, BufferBindingType, BufferUsages, ColorTargetState, Face, @@ -33,8 +33,8 @@ impl PerPixelAdjustShaderRuntime { } impl ShaderRuntime { - pub async fn run_per_pixel_adjust(&self, shaders: &Shaders<'_>, textures: List>, args: Option<&T>) -> List> { - let mut cache = self.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.per_pixel_adjust.pipeline_cache.lock().unwrap_or_else(PoisonError::into_inner); 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 da5fe1c073..c4e8eefca0 100644 --- a/node-graph/libraries/wgpu-executor/src/texture_conversion.rs +++ b/node-graph/libraries/wgpu-executor/src/texture_conversion.rs @@ -1,9 +1,10 @@ -use crate::WgpuExecutor; +use crate::WgpuExecutorHandle; use core_types::Color; use core_types::Ctx; 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::runtime::SourceFuture; use core_types::transform::Footprint; use raster_types::Image; use raster_types::{CPU, GPU, Raster}; @@ -38,6 +39,52 @@ fn upload_to_texture(device: &wgpu::Device, queue: &wgpu::Queue, image: &Raster< ) } +/// Passthrough conversion for GPU `List`s - no conversion needed +impl Convert>, WgpuExecutorHandle> for List> { + fn convert(self, _: Footprint, _converter: WgpuExecutorHandle) -> List> { + self + } +} + +/// Converts a `List>` to `List>` by uploading each image to a texture +impl Convert>, WgpuExecutorHandle> for List> { + fn convert(self, _: Footprint, executor: WgpuExecutorHandle) -> List> { + let device = &executor.context().device; + let queue = executor.context().queue.lock(); + let list = self + .into_iter() + .map(|row| { + let (image, attributes) = row.into_parts(); + let texture = upload_to_texture(device, &queue, &image); + + Item::from_parts(Raster::new_gpu(texture), attributes) + }) + .collect(); + + queue.submit([]); + list + } +} + +/// Converts single CPU raster to GPU by uploading to texture +impl Convert, WgpuExecutorHandle> for Raster { + fn convert(self, _: Footprint, executor: WgpuExecutorHandle) -> Raster { + let device = &executor.context().device; + let queue = executor.context().queue.lock(); + let texture = upload_to_texture(device, &queue, &self); + + queue.submit([]); + Raster::new_gpu(texture) + } +} + +/// Passthrough conversion for CPU `List`s - no conversion needed +impl Convert>, WgpuExecutorHandle> for List> { + fn convert(self, _: Footprint, _converter: WgpuExecutorHandle) -> List> { + self + } +} + /// Converts a Raster texture to Raster by downloading the underlying texture data. /// /// Assumptions: @@ -142,57 +189,11 @@ 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> { - 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> { - let device = &executor.context().device; - let queue = executor.context().queue.lock(); - let list = self - .into_iter() - .map(|row| { - let (image, attributes) = row.into_parts(); - let texture = upload_to_texture(device, &queue, &image); - - Item::from_parts(Raster::new_gpu(texture), attributes) - }) - .collect(); - - queue.submit([]); - 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 { - let device = &executor.context().device; - let queue = executor.context().queue.lock(); - let texture = upload_to_texture(device, &queue, &self); - - queue.submit([]); - Raster::new_gpu(texture) - } -} - -/// 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> { - 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> { - let device = &executor.context().device; - let queue = &executor.context().queue; +impl ConvertAsync>, WgpuExecutorHandle> for List> { + fn convert(self, _: Footprint, executor: WgpuExecutorHandle) -> SourceFuture>> { + let device = executor.context().device.clone(); + let queue = executor.context().queue.lock(); let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("batch_texture_download_encoder"), @@ -203,48 +204,50 @@ impl<'i> Convert>, &'i WgpuExecutor> for List> { for row in self { let (element, attributes) = row.into_parts(); - converters.push(RasterGpuToRasterCpuConverter::new(device, &mut encoder, element)); + converters.push(RasterGpuToRasterCpuConverter::new(&device, &mut encoder, element)); rows_meta.push(Item::from_parts((), attributes)); } queue.submit([encoder.finish()]); - let mut map_futures = Vec::new(); - for converter in converters { - map_futures.push(converter.convert(device)); - } - - let map_results = futures::future::try_join_all(map_futures) - .await - .map_err(|_| "Failed to receive map result") - .expect("Buffer mapping communication failed"); + Box::pin(async move { + let mut map_futures = Vec::new(); + for converter in converters { + map_futures.push(converter.convert(&device)); + } - map_results - .into_iter() - .zip(rows_meta) - .map(|(element, row)| { - let (_, attributes) = row.into_parts(); - Item::from_parts(element, attributes) - }) - .collect() + let map_results = futures::future::try_join_all(map_futures) + .await + .map_err(|_| "Failed to receive map result") + .expect("Buffer mapping communication failed"); + + map_results + .into_iter() + .zip(rows_meta) + .map(|(element, row)| { + let (_, attributes) = row.into_parts(); + Item::from_parts(element, attributes) + }) + .collect() + }) } } /// 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 { - let device = &executor.context().device; - let queue = &executor.context().queue; +impl ConvertAsync, WgpuExecutorHandle> for Raster { + fn convert(self, _: Footprint, executor: WgpuExecutorHandle) -> SourceFuture> { + let device = executor.context().device.clone(); + let queue = executor.context().queue.lock(); let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("single_texture_download_encoder"), }); - let converter = RasterGpuToRasterCpuConverter::new(device, &mut encoder, self); + let converter = RasterGpuToRasterCpuConverter::new(&device, &mut encoder, self); queue.submit([encoder.finish()]); - converter.convert(device).await.expect("Failed to download texture data") + Box::pin(async move { converter.convert(&device).await.expect("Failed to download texture data") }) } } @@ -252,10 +255,10 @@ impl<'i> Convert, &'i WgpuExecutor> for Raster { /// /// Accepts either individual raster data or a `List` of raster elements and converts it to the GPU format using the WgpuExecutor's device and queue. #[node_macro::node(category(""))] -pub async fn upload_texture<'a: 'n, T: Convert>, &'a WgpuExecutor>>( +pub fn upload_texture>, WgpuExecutorHandle>>( _: impl Ctx, #[implementations(List>, List>)] input: T, - executor: &'a WgpuExecutor, + executor: WgpuExecutorHandle, ) -> List> { - input.convert(Footprint::DEFAULT, executor).await + input.convert(Footprint::DEFAULT, executor) } diff --git a/node-graph/node-macro/src/codegen.rs b/node-graph/node-macro/src/codegen.rs index 3df1e855f4..90ecc36217 100644 --- a/node-graph/node-macro/src/codegen.rs +++ b/node-graph/node-macro/src/codegen.rs @@ -1,28 +1,24 @@ +use crate::crate_ident::CrateIdent; use crate::parsing::*; use convert_case::{Case, Casing}; use proc_macro2::TokenStream as TokenStream2; -use quote::{ToTokens, format_ident, quote, quote_spanned}; +use quote::{ToTokens, format_ident, quote}; use std::sync::atomic::AtomicU64; use syn::punctuated::Punctuated; -use syn::spanned::Spanned; -use syn::token::Comma; -use syn::{Error, Ident, PatIdent, Token, WhereClause, WherePredicate, parse_quote}; +use syn::visit::Visit; +use syn::{GenericArgument, GenericParam, Ident, Lifetime, PatIdent, PathArguments, Type, TypeParam, TypeParamBound}; static NODE_ID: AtomicU64 = AtomicU64::new(0); pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn) -> syn::Result { let ParsedNodeFn { - vis, attributes, fn_name, struct_name, mod_name, fn_generics, - where_clause, input, output_type, - is_async, fields, - body, description, .. } = parsed; @@ -80,13 +76,10 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn // Combined struct generic parameters with bounds for struct definition // struct MemoizeNode let struct_generic_params: Vec = data_field_generics.iter().map(|gp| quote!(#gp)).chain(node_generics.iter().map(|id| quote!(#id))).collect(); - let input_ident = &input.pat_ident; - let context_features = &input.context_features; // Regular field idents and names (for function parameters) let field_idents: Vec<_> = regular_fields.iter().map(|f| &f.pat_ident).collect(); - let field_names: Vec<_> = field_idents.iter().map(|pat_ident| &pat_ident.ident).collect(); let regular_field_names: Vec<_> = regular_fields.iter().map(|f| &f.pat_ident.ident).collect(); let data_field_names: Vec<_> = data_fields.iter().map(|f| &f.pat_ident.ident).collect(); @@ -119,34 +112,12 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn quote! { pub(super) #name: #r#gen } }); - let struct_fields = data_field_defs.chain(regular_field_defs); - - let mut future_idents = Vec::new(); - - // Data fields get passed as references to the underlying function - let data_field_idents: Vec<_> = data_fields.iter().map(|f| &f.pat_ident).collect(); - let data_field_types: Vec<_> = data_fields - .iter() - .map(|field| match &field.ty { - ParsedFieldType::Regular(RegularParsedField { ty, .. }) => { - let ty = ty.clone(); - quote!(&#ty) - } - _ => unreachable!("Data fields must be Regular types, not Node types"), - }) - .collect(); - - // Regular fields have types passed to the function - let field_types: Vec<_> = regular_fields - .iter() - .map(|field| match &field.ty { - ParsedFieldType::Regular(RegularParsedField { ty, .. }) => ty.clone(), - 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>), - }, - }) - .collect(); + let async_source = parsed.injects_async_source_fields(); + let slot_value_type = slot_value_type(output_type); + let slot_field = async_source + .then(|| quote! { pub(super) slot: std::sync::Arc>>>> }) + .into_iter(); + let struct_fields = data_field_defs.chain(regular_field_defs).chain(slot_field); // Only regular fields have UI metadata (data fields are internal state) let widget_override: Vec<_> = regular_fields @@ -173,12 +144,13 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn } } ParsedValueSource::Scope(data) => { - if let syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(_), .. }) = data { + if let syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(_), .. }) = &**data { quote!(RegistryValueSource::Scope(#data)) } else { quote!(RegistryValueSource::Scope(#data.as_static_str())) } } + ParsedValueSource::SourceId => quote!(RegistryValueSource::SourceId), _ => quote!(RegistryValueSource::None), }, _ => quote!(RegistryValueSource::None), @@ -233,39 +205,6 @@ 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 name = &field.pat_ident.ident; - match &field.ty { - ParsedFieldType::Regular { .. } => { - quote! { let #name = self.#name.eval(__input.clone()).await; } - } - ParsedFieldType::Node { .. } => { - quote! { let #name = &self.#name; } - } - } - }); - - // Only regular fields can have min/max constraints - let min_max_args = regular_fields.iter().map(|field| match &field.ty { - ParsedFieldType::Regular(RegularParsedField { number_hard_min, number_hard_max, .. }) => { - let name = &field.pat_ident.ident; - let mut tokens = quote!(); - if let Some(min) = number_hard_min { - tokens.extend(quote_spanned! {min.span()=> - let #name = #core_types::misc::Clampable::clamp_hard_min(#name, #min); - }); - } - - if let Some(max) = number_hard_max { - tokens.extend(quote_spanned! {max.span()=> - let #name = #core_types::misc::Clampable::clamp_hard_max(#name, #max); - }); - } - tokens - } - ParsedFieldType::Node { .. } => quote!(), - }); - let all_implementation_types = fields.iter().flat_map(|field| match &field.ty { ParsedFieldType::Regular(RegularParsedField { implementations, .. }) => implementations.iter().cloned().collect::>(), ParsedFieldType::Node(NodeParsedField { implementations, .. }) => implementations @@ -275,61 +214,6 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn }); let all_implementation_types = all_implementation_types.chain(input.implementations.iter().cloned()); - let input_type = &parsed.input.ty; - let mut clauses = Vec::new(); - let mut clampable_clauses = Vec::new(); - - for (field, name) in regular_fields.iter().zip(node_generics.iter()) { - clauses.push(match (&field.ty, *is_async) { - ( - ParsedFieldType::Regular(RegularParsedField { - ty, number_hard_min, number_hard_max, .. - }), - _, - ) => { - let all_lifetime_ty = substitute_lifetimes(ty.clone(), "all"); - let id = future_idents.len(); - let fut_ident = format_ident!("F{}", id); - future_idents.push(fut_ident.clone()); - - // Add Clampable bound if this field uses hard_min or hard_max - if number_hard_min.is_some() || number_hard_max.is_some() { - // The bound applies to the Output type of the future, which is #ty - clampable_clauses.push(quote!(#ty: #core_types::misc::Clampable)); - } - - 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 - ) - } - (ParsedFieldType::Node(NodeParsedField { input_type, output_type, .. }), true) => { - let id = future_idents.len(); - let fut_ident = format_ident!("F{}", id); - future_idents.push(fut_ident.clone()); - - quote!( - #fut_ident: core::future::Future + #core_types::WasmNotSend + 'n, - #name: #core_types::Node<'n, #input_type, Output = #fut_ident > + #core_types::WasmNotSync - ) - } - (ParsedFieldType::Node { .. }, false) => unreachable!("Found node which takes an impl Node<> input but is not async"), - }); - } - let where_clause = where_clause.clone().unwrap_or(WhereClause { - where_token: Token![where](output_type.span()), - predicates: Default::default(), - }); - - let mut struct_where_clause = where_clause.clone(); - let extra_where: Punctuated = parse_quote!( - #(#clauses,)* - #(#clampable_clauses,)* - #output_type: 'n, - ); - struct_where_clause.predicates.extend(extra_where); - // Only regular fields are parameters to new() let new_args = node_generics.iter().zip(regular_field_names.iter()).map(|(r#gen, name)| { quote! { #name: #r#gen } @@ -342,46 +226,16 @@ 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 = data_inits.chain(regular_inits); - - let async_keyword = is_async.then(|| quote!(async)); - let await_keyword = is_async.then(|| quote!(.await)); + let slot_init = async_source.then(|| quote! { slot: Default::default() }).into_iter(); + let all_field_inits = data_inits.chain(regular_inits).chain(slot_init); // 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() && !async_source { quote!(#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]) } else { quote!(#[derive(Debug, Clone)]) }; - // Generate serialize method if serialize attribute is specified - let serialize_impl = if let Some(serialize_fn) = &parsed.attributes.serialize { - let data_field_refs = data_field_names.iter().map(|name| quote!(&self.#name)); - quote! { - fn serialize(&self) -> Option> { - #serialize_fn(#(#data_field_refs),*) - } - } - } else { - quote!() - }; - - 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 { - use #core_types::misc::Clampable; - - #(#eval_args)* - #(#min_max_args)* - self::#fn_name(__input #(, &self.#data_field_names)* #(, #regular_field_names)*) #await_keyword - }) - } - - #serialize_impl - }; - let identifier = format_ident!("{}_proto_ident", fn_name); let identifier_path = match parsed.attributes.path.as_ref() { Some(path) => { @@ -391,8 +245,23 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn None => quote!(std::module_path!()), }; - let register_node_impl = generate_register_node_impl(parsed, &field_names, &struct_name, &identifier)?; + let registry_name = format_ident!("__node_registry_{}_{}", NODE_ID.fetch_add(1, std::sync::atomic::Ordering::SeqCst), struct_name); + let register_node_impl = quote! { + #[cfg(target_family = "wasm")] + #[unsafe(no_mangle)] + extern "C" fn #registry_name() { + register_metadata(); + } + }; let import_name = format_ident!("_IMPORT_STUB_{}", mod_name.to_string().to_case(Case::UpperSnake)); + let node = generate_node_impl(crate_ident, parsed)?; + let node_in_mod = node.in_mod; + let node_top_level = node.top_level; + let entries_name = format_ident!("{}_entries", parsed.fn_name); + let register_entries = match node_in_mod.is_empty() { + true => quote!(), + false => quote!(gcore::registry::NODE_REGISTRY.lock().unwrap().entry(#identifier()).or_default().extend(#entries_name());), + }; let properties = &attributes.properties_string.as_ref().map(|value| quote!(Some(#value))).unwrap_or(quote!(None)); let memoize_flag = attributes.memoize; @@ -428,17 +297,7 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn Ok(quote! { #(#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 - - #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,)*> - #struct_where_clause - { - #eval_impl - } + #node_top_level #cfg const fn #identifier() -> #core_types::ProtoNodeIdentifier { @@ -458,10 +317,8 @@ 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::value::ClonedNode; - use gcore::ops::TypeNode; - use gcore::registry::{NodeMetadata, FieldMetadata, NODE_REGISTRY, NODE_METADATA, DynAnyNode, DowncastBothNode, DynFuture, TypeErasedBox, PanicNode, RegistryValueSource, RegistryWidgetOverride}; + use gcore::{ContextFeature, concrete}; + use gcore::registry::{NodeMetadata, FieldMetadata, NODE_METADATA, RegistryValueSource, RegistryWidgetOverride}; use gcore::ctor::ctor; // Use the types specified in the implementation @@ -484,6 +341,8 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn } } + #node_in_mod + #register_node_impl #[cfg_attr(not(target_family = "wasm"), ctor)] @@ -496,6 +355,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, + async_source_fields: #async_source, fields: vec![ #( FieldMetadata { @@ -519,6 +379,7 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn ], }; NODE_METADATA.lock().unwrap().insert(#identifier(), metadata); + #register_entries } } @@ -619,161 +480,8 @@ fn generate_phantom_data<'a>(fn_generics: impl Iterator Result { - // On native, `register_node` and `register_metadata` run automatically via `#[ctor]`. - // On Wasm, `ctor` isn't available, so this `extern "C"` fn is invoked from JS to register the same way. - // `skip_impl` nodes don't generate a `register_node`, so the shim calls only `register_metadata` for them. - let registry_name = format_ident!("__node_registry_{}_{}", NODE_ID.fetch_add(1, std::sync::atomic::Ordering::SeqCst), struct_name); - let register_node_call = if parsed.attributes.skip_impl { quote!() } else { quote!(register_node();) }; - let wasm_shim = quote! { - #[cfg(target_family = "wasm")] - #[unsafe(no_mangle)] - extern "C" fn #registry_name() { - #register_node_call - register_metadata(); - } - }; - - if parsed.attributes.skip_impl { - return Ok(wasm_shim); - } - - 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 parameter_types: Vec<_> = regular_fields - .iter() - .map(|field| { - match &field.ty { - ParsedFieldType::Regular(RegularParsedField { implementations, ty, .. }) => { - if !implementations.is_empty() { - implementations.iter().map(|ty| (&unit, ty)).collect() - } else { - vec![(&unit, ty)] - } - } - ParsedFieldType::Node(NodeParsedField { - implementations, - input_type, - output_type, - .. - }) => { - if !implementations.is_empty() { - implementations.iter().map(|impl_| (&impl_.input, &impl_.output)).collect() - } else { - vec![(input_type, output_type)] - } - } - } - .into_iter() - .map(|(input, out)| (substitute_lifetimes(input.clone(), "_"), substitute_lifetimes(out.clone(), "_"))) - .collect::>() - }) - .collect(); - - let max_implementations = parameter_types.iter().map(|x| x.len()).chain([parsed.input.implementations.len().max(1)]).max(); - - for i in 0..max_implementations.unwrap_or(0) { - let mut temp_constructors = Vec::new(); - let mut temp_node_io = Vec::new(); - let mut panic_node_types = Vec::new(); - - for (j, types) in parameter_types.iter().enumerate() { - let field_name = field_names[j]; - let (input_type, output_type) = &types[i.min(types.len() - 1)]; - - 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, #output_type, alias: #output_type))); - panic_node_types.push(quote!(#input_type, DynFuture<'static, #output_type>)); - } - let input_type = match parsed.input.implementations.is_empty() { - true => parsed.input.ty.clone(), - false => parsed.input.implementations[i.min(parsed.input.implementations.len() - 1)].clone(), - }; - constructors.push(quote!( - ( - |args| { - Box::pin(async move { - #(#temp_constructors;)* - let node = #struct_name::new(#(#field_names,)*); - // try polling futures - let any: DynAnyNode<#input_type, _, _> = DynAnyNode::new(node); - Box::new(any) as TypeErasedBox<'_> - }) - }, { - let node = #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 - - } - ) - )); - } - Ok(quote! { - #[cfg_attr(not(target_family = "wasm"), ctor)] - fn register_node() { - let mut registry = NODE_REGISTRY.lock().unwrap(); - registry.insert( - #identifier(), - vec![ - #(#constructors,)* - ] - ); - } - - #wasm_shim - }) -} - -use crate::crate_ident::CrateIdent; use crate::shader_nodes::{ShaderCodegen, ShaderTokens}; use syn::visit_mut::VisitMut; -use syn::{GenericArgument, Lifetime, Type}; - -struct LifetimeReplacer(&'static str); - -impl VisitMut for LifetimeReplacer { - fn visit_lifetime_mut(&mut self, lifetime: &mut Lifetime) { - lifetime.ident = Ident::new(self.0, lifetime.ident.span()); - } - - fn visit_type_mut(&mut self, ty: &mut Type) { - match ty { - Type::Reference(type_reference) => { - if let Some(lifetime) = &mut type_reference.lifetime { - self.visit_lifetime_mut(lifetime); - } - self.visit_type_mut(&mut type_reference.elem); - } - _ => syn::visit_mut::visit_type_mut(self, ty), - } - } - - fn visit_generic_argument_mut(&mut self, arg: &mut GenericArgument) { - if let GenericArgument::Lifetime(lifetime) = arg { - self.visit_lifetime_mut(lifetime); - } else { - syn::visit_mut::visit_generic_argument_mut(self, arg); - } - } -} - -#[must_use] -fn substitute_lifetimes(mut ty: Type, lifetime: &'static str) -> Type { - LifetimeReplacer(lifetime).visit_type_mut(&mut ty); - ty -} /// Get only the necessary generics. struct FilterUsedGenerics { @@ -856,7 +564,7 @@ impl FilterUsedGenerics { } /// Check if a type contains a reference to a specific identifier (e.g., a generic type parameter) -fn type_contains_ident(ty: &Type, ident: &Ident) -> bool { +pub(crate) fn type_contains_ident(ty: &Type, ident: &Ident) -> bool { struct IdentChecker<'a> { target: &'a Ident, found: bool, @@ -874,3 +582,669 @@ fn type_contains_ident(ty: &Type, ident: &Ident) -> bool { syn::visit::visit_type(&mut checker, ty); checker.found } + +pub(crate) struct NodeImplTokens { + pub(crate) in_mod: TokenStream2, + pub(crate) top_level: TokenStream2, +} + +pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn) -> syn::Result { + let core_types = crate_ident.gcore()?; + + let ctx_param = context_param(parsed); + let ctx_ident = match ctx_param { + Some(ctx_param) => ctx_param.ident.clone(), + None => format_ident!("__Ctx"), + }; + let async_fn = parsed.is_async; + let future_kernel = is_source_kernel(&parsed.output_type); + let async_source = async_fn || future_kernel; + if async_fn && parsed.fields.iter().any(|field| matches!(field.ty, ParsedFieldType::Node(_))) { + return Ok(NodeImplTokens { + in_mod: quote!(), + top_level: quote!(), + }); + } + let snapshot_ctx = async_fn && matches!(&parsed.input.ty, Type::Path(path) if path.path.segments.last().is_some_and(|segment| segment.ident == "CtxSnapshot")); + + let mut ctx_bounds: Vec = match ctx_param { + Some(ctx_param) => ctx_param + .bounds + .iter() + .filter_map(|bound| match bound { + TypeParamBound::Lifetime(_) => None, + bound => Some(desugar_extract_lifetime(bound, core_types)), + }) + .collect(), + None => vec![quote!(#core_types::Ctx)], + }; + if async_source && !snapshot_ctx { + ctx_bounds.push(quote!(#core_types::context::DeriveCtx)); + } + if snapshot_ctx { + ctx_bounds.extend([ + quote!(#core_types::context::DeriveCtx), + quote!(#core_types::context::ExtractFootprint), + quote!(#core_types::context::ExtractRealTime), + quote!(#core_types::context::ExtractAnimationTime), + quote!(#core_types::context::ExtractPointerPosition), + quote!(#core_types::context::ExtractIndex), + quote!(#core_types::context::ExtractPosition), + ]); + } + + let derives = ctx_param.is_some_and(|ctx_param| { + ctx_param.bounds.iter().any(|bound| match bound { + TypeParamBound::Trait(trait_bound) => trait_bound.path.segments.last().is_some_and(|segment| segment.ident == "DeriveCtx"), + _ => false, + }) + }); + + let ctx_generic = match ctx_bounds.is_empty() { + true => quote!(#ctx_ident), + false => quote!(#ctx_ident: #(#ctx_bounds)+*), + }; + let mut generics: Vec = parsed + .fn_generics + .iter() + .map(|param| match param { + GenericParam::Type(type_param) if Some(&type_param.ident) == ctx_param.map(|ctx_param| &ctx_param.ident) => ctx_generic.clone(), + param => quote!(#param), + }) + .collect(); + if ctx_param.is_none() { + generics.push(ctx_generic); + } + + let fn_name = &parsed.fn_name; + let mod_name = format_ident!("_{}_mod", parsed.mod_name); + let struct_name = format_ident!("{}Node", parsed.struct_name); + let output_type = &parsed.output_type; + let trait_output = slot_value_type(&parsed.output_type); + let raw_lazy = matches!(kernel_kind(&parsed.output_type), KernelKind::Poll(_)); + let injected_name = |ident: &Ident| async_source && (ident == "_runtime" || ident == "_source"); + let where_predicates: Vec = parsed.where_clause.iter().flat_map(|clause| clause.predicates.iter()).map(|predicate| quote!(#predicate)).collect(); + + let (data_fields, regular_fields): (Vec<_>, Vec<_>) = parsed.fields.iter().partition(|field| field.is_data_field); + + let data_field_generic_idents: Vec = parsed + .fn_generics + .iter() + .filter_map(|generic| match generic { + GenericParam::Type(type_param) => Some(type_param.ident.clone()), + _ => None, + }) + .filter(|ident| { + data_fields.iter().any(|field| match &field.ty { + ParsedFieldType::Regular(RegularParsedField { ty, .. }) => crate::codegen::type_contains_ident(ty, ident), + _ => false, + }) + }) + .collect(); + + let node_generics: Vec = regular_fields.iter().enumerate().map(|(index, _)| format_ident!("Node{}", index)).collect(); + let struct_type_params: Vec = data_field_generic_idents.iter().cloned().chain(node_generics.iter().cloned()).collect(); + + let data_names: Vec<&Ident> = data_fields.iter().map(|field| &field.pat_ident.ident).collect(); + let data_params = data_fields.iter().map(|field| { + let pat = &field.pat_ident; + let ParsedFieldType::Regular(RegularParsedField { ty, .. }) = &field.ty else { + unreachable!("data fields are regular types"); + }; + quote!(#pat: &#ty) + }); + + let lazy_bound = |output_type: &Type| match derives { + true => quote!(for<'__derived> #core_types::node::Node<#core_types::context::Derived<'__derived, #ctx_ident>, Output = #output_type>), + false => quote!(#core_types::node::Node<#ctx_ident, Output = #output_type>), + }; + + let kernel_params = regular_fields.iter().filter(|field| !injected_name(&field.pat_ident.ident)).map(|field| { + let pat = &field.pat_ident; + match &field.ty { + ParsedFieldType::Regular(RegularParsedField { ty, .. }) => quote!(#pat: #ty), + ParsedFieldType::Node(NodeParsedField { output_type, .. }) if raw_lazy => { + let bound = lazy_bound(output_type); + quote!(#pat: &impl #bound) + } + ParsedFieldType::Node(NodeParsedField { output_type, .. }) => { + let bound = lazy_bound(output_type); + quote!(#pat: #core_types::node::LazyInput<'_, impl #bound>) + } + } + }); + + let node_bounds = regular_fields.iter().zip(&node_generics).map(|(field, node_generic)| match &field.ty { + ParsedFieldType::Regular(RegularParsedField { ty, .. }) => quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = #ty>), + ParsedFieldType::Node(NodeParsedField { output_type, .. }) => { + let bound = lazy_bound(output_type); + quote!(#node_generic: #bound) + } + }); + + let mut async_bounds = match (async_fn, future_kernel) { + (false, false) => Vec::new(), + (false, true) => vec![quote!(#trait_output: Clone)], + (true, _) => { + let output_clone = std::iter::once(quote!(#trait_output: Clone)); + let value_clones = regular_fields.iter().filter_map(|field| match &field.ty { + ParsedFieldType::Regular(RegularParsedField { ty, .. }) => Some(quote!(#ty: Clone)), + _ => None, + }); + let data_clones = data_fields.iter().filter_map(|field| match &field.ty { + ParsedFieldType::Regular(RegularParsedField { ty, .. }) => Some(quote!(#ty: Clone)), + _ => None, + }); + output_clone.chain(value_clones).chain(data_clones).collect() + } + }; + if async_source { + async_bounds.push(quote!(for<'__derived> #core_types::context::Derived<'__derived, #ctx_ident>: #core_types::CacheHash)); + } + + let clampable_bounds = regular_fields.iter().filter_map(|field| { + let ParsedFieldType::Regular(RegularParsedField { + ty, number_hard_min, number_hard_max, .. + }) = &field.ty + else { + return None; + }; + (number_hard_min.is_some() || number_hard_max.is_some()).then(|| quote!(#ty: #core_types::misc::Clampable)) + }); + + let eval_values = regular_fields.iter().enumerate().map(|(index, field)| { + let name = &field.pat_ident.ident; + match &field.ty { + ParsedFieldType::Regular(_) => quote! { + let #name = match __cell.eval_input(#index, &self.#name, __input) { + Ok(value) => value, + Err(interrupt) => return interrupt.into(), + }; + }, + ParsedFieldType::Node(_) if raw_lazy => quote!(), + ParsedFieldType::Node(_) => quote! { + let #name = #core_types::node::LazyInput::new(&self.#name, &__cell, #index); + }, + } + }); + + let clamps = regular_fields.iter().filter_map(|field| { + let ParsedFieldType::Regular(RegularParsedField { number_hard_min, number_hard_max, .. }) = &field.ty else { + return None; + }; + let name = &field.pat_ident.ident; + let mut tokens = quote!(); + if let Some(min) = number_hard_min { + tokens.extend(quote!(let #name = #core_types::misc::Clampable::clamp_hard_min(#name, #min);)); + } + if let Some(max) = number_hard_max { + tokens.extend(quote!(let #name = #core_types::misc::Clampable::clamp_hard_max(#name, #max);)); + } + (!tokens.is_empty()).then_some(tokens) + }); + + let call_args = regular_fields.iter().filter(|field| !injected_name(&field.pat_ident.ident)).map(|field| { + let name = &field.pat_ident.ident; + match &field.ty { + ParsedFieldType::Node(_) if raw_lazy => quote!(&self.#name), + _ => quote!(#name), + } + }); + + let value_field_names: Vec<&Ident> = regular_fields + .iter() + .filter(|field| matches!(field.ty, ParsedFieldType::Regular(_))) + .map(|field| &field.pat_ident.ident) + .collect(); + + let extent_impl = match &parsed.attributes.extent { + Some(path) => quote! { + fn extent(&self, __input: &#ctx_ident) -> #core_types::gpoll::GPoll<#core_types::gpoll::Extent> { + #path(self, __input) + } + }, + None if value_field_names.is_empty() => quote!(), + None => { + let first = value_field_names[0]; + let mut meet = quote!(self.#first.extent(__input)); + for name in &value_field_names[1..] { + meet = quote!(#core_types::gpoll::Extent::meet(#meet, self.#name.extent(__input))); + } + quote! { + fn extent(&self, __input: &#ctx_ident) -> #core_types::gpoll::GPoll<#core_types::gpoll::Extent> { + #meet + } + } + } + }; + + let serialize_impl = match &parsed.attributes.serialize { + Some(path) => { + let data_refs = data_names.iter().map(|name| quote!(&self.#name)); + quote! { + fn serialize(&self) -> Option<::std::sync::Arc> { + #path(#(#data_refs),*) + } + } + } + None => quote!(), + }; + + let batch_impl = match &parsed.attributes.batch { + Some(path) => quote! { + fn eval_batch<'__batch>( + &self, + __input: &'__batch #ctx_ident, + __range: ::std::ops::Range, + __scratch: Option<&'__batch mut [::std::mem::MaybeUninit]>, + ) -> #core_types::node::BatchStatus<'__batch, Self::Output> + where + #ctx_ident: #core_types::context::InjectIndex + Copy, + { + #path(self, __input, __range, __scratch) + } + }, + None => quote!(), + }; + + let ctx_pat = &parsed.input.pat_ident; + let fn_where = &parsed.where_clause; + let body = &parsed.body; + let vis = &parsed.vis; + let kernel_fields: Vec<&&ParsedField> = regular_fields.iter().filter(|field| !injected_name(&field.pat_ident.ident)).collect(); + let kernel = match async_fn { + false => quote! { + #[allow(clippy::too_many_arguments)] + #vis fn #fn_name<#(#generics,)*>(#ctx_pat: &#ctx_ident #(, #data_params)* #(, #kernel_params)*) -> #output_type #fn_where #body + }, + true => { + let kernel_generics = parsed.fn_generics.iter().filter(|param| match param { + GenericParam::Type(type_param) => Some(&type_param.ident) != ctx_param.map(|ctx_param| &ctx_param.ident), + _ => true, + }); + let snapshot_param = snapshot_ctx.then(|| quote!(#ctx_pat: #core_types::context::CtxSnapshot)).into_iter(); + let data_kernel_params = data_fields.iter().map(|field| { + let pat = &field.pat_ident; + let ParsedFieldType::Regular(RegularParsedField { ty, .. }) = &field.ty else { + unreachable!("data fields are regular types"); + }; + quote!(#pat: #ty) + }); + let value_kernel_params = kernel_fields.iter().map(|field| { + let pat = &field.pat_ident; + let ParsedFieldType::Regular(RegularParsedField { ty, .. }) = &field.ty else { + unreachable!("async source fields are eager values"); + }; + quote!(#pat: #ty) + }); + let params = snapshot_param.chain(data_kernel_params).chain(value_kernel_params); + quote! { + #[allow(clippy::too_many_arguments)] + #vis async fn #fn_name<#(#kernel_generics,)*>(#(#params),*) -> #output_type #fn_where #body + } + } + }; + let cell_constructor = match parsed.attributes.no_partial { + true => quote!(#core_types::node::StatusCell::no_partial()), + false => quote!(#core_types::node::StatusCell::new()), + }; + let kernel_call = quote!(self::#fn_name(__input #(, &self.#data_names)* #(, #call_args)*)); + let lift = match kernel_kind(&parsed.output_type) { + KernelKind::Interrupt(_) => quote! { + match #kernel_call { + Ok(value) => __cell.finish(value), + Err(interrupt) => interrupt.into(), + } + }, + KernelKind::Poll(_) => quote!(__cell.merge(#kernel_call)), + _ => quote!(__cell.finish(#kernel_call)), + }; + + let placeholder_value_names: Vec<&Ident> = kernel_fields + .iter() + .filter(|field| matches!(field.ty, ParsedFieldType::Regular(_))) + .map(|field| &field.pat_ident.ident) + .collect(); + let inflight = match &parsed.attributes.placeholder { + Some(path) => quote!(__cell.merge(#core_types::gpoll::GPoll::Partial(#path(#(&#placeholder_value_names),*)))), + None => quote!(#core_types::gpoll::GPoll::Pending), + }; + let slot_check = quote! { + let __scope = #core_types::context::DeriveCtx::scope(__input).excluding(_source); + let __key = #core_types::registry::cache_key(&#core_types::context::DeriveCtx::with_scope(__input, &__scope)); + { + let __entries = self.slot.lock().unwrap_or_else(std::sync::PoisonError::into_inner); + if let Some(__state) = __entries.get(&__key) { + return match __state { + Some(value) => __cell.merge(value.clone()), + None => #inflight, + }; + } + } + }; + let future_completion = |payload: &Type| match kernel_kind(payload) { + KernelKind::Poll(_) => quote!(__future.await), + KernelKind::Interrupt(_) => quote! { + match __future.await { + Ok(value) => #core_types::gpoll::GPoll::Final(value), + Err(interrupt) => interrupt.into(), + } + }, + _ => quote!(#core_types::gpoll::GPoll::Final(__future.await)), + }; + let spawn_tail = |completion: TokenStream2, fallback: TokenStream2| { + quote! { + self.slot.lock().unwrap_or_else(std::sync::PoisonError::into_inner).insert(__key, None); + let __slot = std::sync::Arc::clone(&self.slot); + if _runtime.0.spawn(_source, Box::pin(async move { + let __value = #completion; + __slot.lock().unwrap_or_else(std::sync::PoisonError::into_inner).insert(__key, Some(__value)); + })) { + let __entries = self.slot.lock().unwrap_or_else(std::sync::PoisonError::into_inner); + if let Some(Some(__value)) = __entries.get(&__key) { + return __cell.merge(__value.clone()); + } + } + #fallback + } + }; + let eval_tail = match (async_fn, future_kernel) { + (false, false) => lift, + (true, _) => { + let kernel_value_names: Vec<&Ident> = kernel_fields.iter().map(|field| &field.pat_ident.ident).collect(); + let snapshot_binding = snapshot_ctx.then(|| quote!(let __snapshot = #core_types::context::CtxSnapshot::capture(__input);)).into_iter(); + let snapshot_arg = snapshot_ctx.then(|| quote!(__snapshot)).into_iter(); + let future_args = snapshot_arg + .chain(data_names.iter().map(|name| quote!(self.#name.clone()))) + .chain(kernel_value_names.iter().map(|name| quote!(#name.clone()))); + let completion = future_completion(&parsed.output_type); + let tail = spawn_tail(completion, inflight.clone()); + quote! { + #slot_check + #(#snapshot_binding)* + let __future = self::#fn_name(#(#future_args),*); + #tail + } + } + (false, true) => { + let (placeholder_binding, spawn_return) = match &parsed.attributes.placeholder { + Some(path) => ( + quote!(let __placeholder = #path(#(&#placeholder_value_names),*);), + quote!(__cell.merge(#core_types::gpoll::GPoll::Partial(__placeholder))), + ), + None => (quote!(), quote!(#core_types::gpoll::GPoll::Pending)), + }; + let acquire = match kernel_kind(&parsed.output_type) { + KernelKind::FutureInterrupt(_) => quote! { + let __future = match #kernel_call { + Ok(future) => future, + Err(interrupt) => return interrupt.into(), + }; + }, + _ => quote!(let __future = #kernel_call;), + }; + let payload = match kernel_kind(&parsed.output_type) { + KernelKind::Future(payload) | KernelKind::FutureInterrupt(payload) => payload, + _ => unreachable!("guarded by future_kernel"), + }; + let completion = future_completion(&payload); + let tail = spawn_tail(completion, spawn_return); + quote! { + #slot_check + #placeholder_binding + #acquire + #tail + } + } + }; + + let entries = entries_tokens(parsed, &struct_name, &data_field_generic_idents, ®ular_fields); + let cfg = crate::shader_nodes::modify_cfg(&parsed.attributes); + + let top_level = quote! { + #cfg + #[automatically_derived] + impl<#(#generics,)* #(#node_generics,)*> #core_types::node::Node<#ctx_ident> for #mod_name::#struct_name<#(#struct_type_params,)*> + where + #(#node_bounds,)* + #(#clampable_bounds,)* + #(#async_bounds,)* + #(#where_predicates,)* + { + type Output = #trait_output; + + fn eval(&self, __input: &#ctx_ident) -> #core_types::gpoll::GPoll { + let __cell = #cell_constructor; + #(#eval_values)* + #(#clamps)* + #eval_tail + } + + #extent_impl + + #serialize_impl + + #batch_impl + } + }; + + Ok(NodeImplTokens { + in_mod: entries, + top_level: quote! { + #kernel + + #top_level + }, + }) +} + +pub(crate) fn slot_value_type(output: &Type) -> Type { + match kernel_kind(output) { + KernelKind::Plain => output.clone(), + KernelKind::Poll(inner) | KernelKind::Interrupt(inner) => inner, + KernelKind::Future(payload) | KernelKind::FutureInterrupt(payload) => match kernel_kind(&payload) { + KernelKind::Poll(inner) | KernelKind::Interrupt(inner) => inner, + _ => payload, + }, + } +} + +pub(crate) fn is_source_kernel(output: &Type) -> bool { + matches!(kernel_kind(output), KernelKind::Future(_) | KernelKind::FutureInterrupt(_)) +} + +enum KernelKind { + Plain, + Interrupt(Type), + Poll(Type), + Future(Type), + FutureInterrupt(Type), +} + +fn source_future_payload(segment: &syn::PathSegment) -> Type { + let PathArguments::AngleBracketed(args) = &segment.arguments else { + return syn::parse_quote!(()); + }; + args.args + .iter() + .find_map(|argument| match argument { + GenericArgument::Type(ty) => Some(ty.clone()), + _ => None, + }) + .unwrap_or_else(|| syn::parse_quote!(())) +} + +fn kernel_kind(output: &Type) -> KernelKind { + let plain = || KernelKind::Plain; + let Type::Path(path) = output else { return plain() }; + let Some(segment) = path.path.segments.last() else { return plain() }; + match segment.ident.to_string().as_str() { + "GPoll" => { + let PathArguments::AngleBracketed(args) = &segment.arguments else { return plain() }; + let inner = args.args.iter().find_map(|argument| match argument { + GenericArgument::Type(ty) => Some(ty.clone()), + _ => None, + }); + inner.map(KernelKind::Poll).unwrap_or_else(plain) + } + "SourceFuture" => KernelKind::Future(source_future_payload(segment)), + "Result" => { + let PathArguments::AngleBracketed(args) = &segment.arguments else { return plain() }; + let mut types = args.args.iter().filter_map(|argument| match argument { + GenericArgument::Type(ty) => Some(ty), + _ => None, + }); + let (Some(inner), Some(Type::Path(error_path))) = (types.next(), types.next()) else { + return plain(); + }; + if error_path.path.segments.last().is_none_or(|segment| segment.ident != "Interrupt") { + return plain(); + } + if let Type::Path(inner_path) = inner + && let Some(inner_segment) = inner_path.path.segments.last() + && inner_segment.ident == "SourceFuture" + { + return KernelKind::FutureInterrupt(source_future_payload(inner_segment)); + } + KernelKind::Interrupt(inner.clone()) + } + _ => plain(), + } +} + +fn context_param(parsed: &ParsedNodeFn) -> Option<&TypeParam> { + let Type::Path(path) = &parsed.input.ty else { + return None; + }; + let ident = path.path.get_ident()?; + parsed.fn_generics.iter().find_map(|param| match param { + GenericParam::Type(type_param) if &type_param.ident == ident => Some(type_param), + _ => None, + }) +} + +fn type_disqualifies(ty: &Type) -> bool { + struct Disqualifier { + found: bool, + } + + impl<'ast> Visit<'ast> for Disqualifier { + fn visit_type_reference(&mut self, _: &'ast syn::TypeReference) { + self.found = true; + } + + fn visit_type_impl_trait(&mut self, _: &'ast syn::TypeImplTrait) { + self.found = true; + } + + fn visit_lifetime(&mut self, _: &'ast Lifetime) { + self.found = true; + } + } + + let mut visitor = Disqualifier { found: false }; + visitor.visit_type(ty); + visitor.found +} + +fn desugar_extract_lifetime(bound: &TypeParamBound, core_types: &TokenStream2) -> TokenStream2 { + let TypeParamBound::Trait(trait_bound) = bound else { + return quote!(#bound); + }; + let Some(segment) = trait_bound.path.segments.last() else { + return quote!(#bound); + }; + if segment.ident != "ExtractArena" { + return quote!(#bound); + } + let PathArguments::AngleBracketed(args) = &segment.arguments else { + return quote!(#bound); + }; + if args.args.len() != 1 { + return quote!(#bound); + } + let Some(GenericArgument::Lifetime(lifetime)) = args.args.first() else { + return quote!(#bound); + }; + quote!(#core_types::context::ExtractArena) +} + +fn entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, data_field_generic_idents: &[Ident], regular_fields: &[&ParsedField]) -> TokenStream2 { + if !data_field_generic_idents.is_empty() { + return quote!(); + } + let Some(rows) = implementation_rows(parsed, regular_fields) else { + return quote!(); + }; + let rows: Vec<&Vec> = rows.iter().filter(|row| row.iter().all(|ty| !type_disqualifies(ty))).collect(); + if rows.is_empty() { + return quote!(); + } + + let fn_name = &parsed.fn_name; + let entries_name = format_ident!("{}_entries", fn_name); + let arity = regular_fields.len(); + let names: Vec<&Ident> = regular_fields.iter().map(|field| &field.pat_ident.ident).collect(); + + let entries = rows.iter().map(|row| { + let types = row.iter(); + let edge_types = row.iter().map(|ty| quote!(gcore::registry::SharedEdge>)); + let output = quote!(<#struct_name<#(#edge_types),*> as gcore::node::Node>>::Output); + let downcasts = names.iter().zip(row.iter()).map(|(name, ty)| quote!(let #name = inputs.next().unwrap().downcast::<#ty>()?;)); + quote! { + gcore::registry::RegistryEntry { + io: gcore::registry::NodeIOTypes::new( + gcore::concrete!(gcore::context::ContextImpl<'static>), + gcore::concrete!(#output), + vec![#(gcore::registry::edge_type::<#types>()),*], + ), + constructor: |inputs| { + if inputs.len() != #arity { + return Err(gcore::registry::ConstructionError::Arity { expected: #arity, got: inputs.len() }); + } + let mut inputs = inputs.into_iter(); + #(#downcasts)* + Ok(gcore::registry::EdgeHandle::new(::std::sync::Arc::new(#struct_name::new(#(#names),*)) as ::std::sync::Arc>)) + }, + } + } + }); + + quote! { + pub fn #entries_name() -> ::std::vec::Vec { + vec![#(#entries),*] + } + } +} + +fn implementation_rows(parsed: &ParsedNodeFn, regular_fields: &[&ParsedField]) -> Option>> { + let ctx_ident = context_param(parsed).map(|ctx| ctx.ident.clone()); + let open_generics: Vec<&Ident> = parsed + .fn_generics + .iter() + .filter_map(|param| match param { + GenericParam::Type(type_param) if Some(&type_param.ident) != ctx_ident.as_ref() => Some(&type_param.ident), + _ => None, + }) + .collect(); + + let candidates: Vec> = regular_fields + .iter() + .map(|field| match &field.ty { + ParsedFieldType::Regular(RegularParsedField { ty, implementations, .. }) => match implementations.is_empty() { + false => Some(implementations.iter().cloned().collect()), + true => open_generics.iter().all(|generic| !crate::codegen::type_contains_ident(ty, generic)).then(|| vec![ty.clone()]), + }, + ParsedFieldType::Node(NodeParsedField { output_type, implementations, .. }) => match implementations.is_empty() { + false => Some(implementations.iter().map(|implementation| implementation.output.clone()).collect()), + true => open_generics + .iter() + .all(|generic| !crate::codegen::type_contains_ident(output_type, generic)) + .then(|| vec![output_type.clone()]), + }, + }) + .collect::>()?; + + let row_count = candidates.iter().map(|types| types.len()).max().unwrap_or(1).max(1); + Some((0..row_count).map(|row| candidates.iter().map(|types| types[row.min(types.len() - 1)].clone()).collect()).collect()) +} diff --git a/node-graph/node-macro/src/parsing.rs b/node-graph/node-macro/src/parsing.rs index 09c1217aeb..605b2ba91c 100644 --- a/node-graph/node-macro/src/parsing.rs +++ b/node-graph/node-macro/src/parsing.rs @@ -56,6 +56,14 @@ pub(crate) struct NodeFnAttributes { pub(crate) memoize: bool, /// Whether this node provides a scope pub(crate) inject_scope: bool, + /// Function producing a stand-in value while an async source node's real value is in flight + pub(crate) placeholder: Option, + /// Function overriding the generated `extent` method + pub(crate) extent: Option, + /// Function overriding the generated `eval_batch` method + pub(crate) batch: Option, + /// Whether partial upstream values are mapped to `Pending` instead of flowing into this node + pub(crate) no_partial: bool, } #[derive(Clone, Debug, Default)] @@ -63,7 +71,8 @@ pub enum ParsedValueSource { #[default] None, Default(TokenStream2), - Scope(Expr), + Scope(Box), + SourceId, } // #[widget(ParsedWidgetOverride::Hidden)] @@ -311,6 +320,10 @@ impl Parse for NodeFnAttributes { let mut serialize = None; let mut memoize = false; let mut inject_scope = false; + let mut placeholder = None; + let mut extent = None; + let mut batch = None; + let mut no_partial = false; let content = input; // let content; @@ -453,13 +466,63 @@ impl Parse for NodeFnAttributes { } inject_scope = true; } + // Function producing a stand-in value for an async source node while the spawned future is in flight. + // The node reports `Partial` with the stand-in until the real value lands; without a placeholder it reports `Pending`. + // + // Example usage: + // #[node_macro::node(..., placeholder(empty_image), ...)] + "placeholder" => { + let meta = meta.require_list()?; + if placeholder.is_some() { + return Err(Error::new_spanned(meta, "Multiple 'placeholder' attributes are not allowed")); + } + let parsed_path: Path = meta + .parse_args() + .map_err(|_| Error::new_spanned(meta, "Expected a valid path for 'placeholder', e.g., placeholder(empty_image)"))?; + placeholder = Some(parsed_path); + } + // Function overriding the generated `extent` method, replacing the default meet over the node's inputs. + // + // Example usage: + // #[node_macro::node(..., extent(my_extent), ...)] + "extent" => { + let meta = meta.require_list()?; + if extent.is_some() { + return Err(Error::new_spanned(meta, "Multiple 'extent' attributes are not allowed")); + } + let parsed_path: Path = meta.parse_args().map_err(|_| Error::new_spanned(meta, "Expected a valid path for 'extent', e.g., extent(my_extent)"))?; + extent = Some(parsed_path); + } + // Function overriding the generated `eval_batch` method, replacing the trait's per-lane spec loop. + // + // Example usage: + // #[node_macro::node(..., batch(my_batch), ...)] + "batch" => { + let meta = meta.require_list()?; + if batch.is_some() { + return Err(Error::new_spanned(meta, "Multiple 'batch' attributes are not allowed")); + } + let parsed_path: Path = meta.parse_args().map_err(|_| Error::new_spanned(meta, "Expected a valid path for 'batch', e.g., batch(my_batch)"))?; + batch = Some(parsed_path); + } + // Instructs the generated eval to report `Pending` instead of passing partial upstream values into this node. + // + // Example usage: + // #[node_macro::node(..., no_partial, ...)] + "no_partial" => { + let path = meta.require_path_only()?; + if no_partial { + return Err(Error::new_spanned(path, "Multiple 'no_partial' attributes are not allowed")); + } + no_partial = true; + } _ => { return Err(Error::new_spanned( meta, indoc!( r#" Unsupported attribute in `node`. - Supported attributes are 'category', 'name', 'path', 'skip_impl', 'properties', 'cfg', 'shader_node', 'serialize', 'memoize', and 'inject_scope'. + Supported attributes are 'category', 'name', 'path', 'skip_impl', 'properties', 'cfg', 'shader_node', 'serialize', 'memoize', 'inject_scope', 'placeholder', 'extent', 'batch', and 'no_partial'. Example usage: #[node_macro::node(..., name("Test Node"), ...)] "# @@ -493,6 +556,10 @@ impl Parse for NodeFnAttributes { serialize, memoize, inject_scope, + placeholder, + extent, + batch, + no_partial, }) } } @@ -723,7 +790,7 @@ fn parse_field(pat_ident: PatIdent, ty: Type, attrs: &[Attribute]) -> syn::Resul let value_source = match (default_value, scope) { (Some(_), Some(_)) => return Err(Error::new_spanned(&pat_ident, "Cannot have both `default` and `scope` attributes")), (Some(default_value), _) => ParsedValueSource::Default(default_value), - (_, Some(scope)) => ParsedValueSource::Scope(scope), + (_, Some(scope)) => ParsedValueSource::Scope(Box::new(scope)), _ => ParsedValueSource::None, }; @@ -931,6 +998,10 @@ pub fn new_node_fn(attr: TokenStream2, item: TokenStream2) -> syn::Result bool { + self.is_async || crate::codegen::is_source_kernel(&self.output_type) + } + + pub fn inject_async_source_fields(&mut self, core_types: &TokenStream2) { + let hidden_field = |name: &str, ty: Type, value_source: ParsedValueSource| ParsedField { + 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::Hidden, + ty: ParsedFieldType::Regular(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, + }), + number_display_decimal_places: None, + number_step: None, + unit: None, + is_data_field: false, + }; + self.fields.push(hidden_field( + "_runtime", + parse_quote!(#core_types::runtime::RuntimeHandle), + ParsedValueSource::Scope(Box::new(parse_quote!("graphene_std::runtime::RuntimeNode"))), + )); + self.fields.push(hidden_field("_source", parse_quote!(#core_types::SourceId), ParsedValueSource::SourceId)); + } } #[cfg(test)] @@ -1082,6 +1194,10 @@ mod tests { serialize: None, memoize: false, inject_scope: false, + placeholder: None, + extent: None, + batch: None, + no_partial: false, }, fn_name: Ident::new("add", Span::call_site()), struct_name: Ident::new("Add", Span::call_site()), @@ -1152,6 +1268,10 @@ mod tests { serialize: None, memoize: false, inject_scope: false, + placeholder: None, + extent: None, + batch: None, + no_partial: false, }, fn_name: Ident::new("transform", Span::call_site()), struct_name: Ident::new("Transform", Span::call_site()), @@ -1236,6 +1356,10 @@ mod tests { serialize: None, memoize: false, inject_scope: false, + placeholder: None, + extent: None, + batch: None, + no_partial: false, }, fn_name: Ident::new("circle", Span::call_site()), struct_name: Ident::new("Circle", Span::call_site()), @@ -1302,6 +1426,10 @@ mod tests { serialize: None, memoize: false, inject_scope: false, + placeholder: None, + extent: None, + batch: None, + no_partial: false, }, fn_name: Ident::new("levels", Span::call_site()), struct_name: Ident::new("Levels", Span::call_site()), @@ -1380,6 +1508,10 @@ mod tests { serialize: None, memoize: false, inject_scope: false, + placeholder: None, + extent: None, + batch: None, + no_partial: false, }, fn_name: Ident::new("add", Span::call_site()), struct_name: Ident::new("Add", Span::call_site()), @@ -1461,6 +1593,10 @@ mod tests { serialize: None, memoize: false, inject_scope: false, + placeholder: None, + extent: None, + batch: None, + no_partial: false, }, fn_name: Ident::new("load_image", Span::call_site()), struct_name: Ident::new("LoadImage", Span::call_site()), @@ -1527,6 +1663,10 @@ mod tests { serialize: None, memoize: false, inject_scope: false, + placeholder: None, + extent: None, + batch: None, + no_partial: false, }, fn_name: Ident::new("custom_node", Span::call_site()), struct_name: Ident::new("CustomNode", Span::call_site()), diff --git a/node-graph/node-macro/src/shader_nodes/per_pixel_adjust.rs b/node-graph/node-macro/src/shader_nodes/per_pixel_adjust.rs index 97f87fc4db..1ec06a28e8 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 @@ -146,7 +146,7 @@ impl PerPixelAdjustCodegen<'_> { ParamType::Uniform => quote!(uniform.#ident), }) .collect::>(); - let context = quote!(()); + let context = quote!(&()); let entry_point_mod = &self.entry_point_mod; let entry_point_name = &self.entry_point_name_ident; @@ -231,9 +231,9 @@ impl PerPixelAdjustCodegen<'_> { description: "".to_string(), widget_override: Default::default(), ty: ParsedFieldType::Regular(RegularParsedField { - ty: parse_quote!(&'a WgpuExecutor), + ty: parse_quote!(#wgpu_executor::WgpuExecutorHandle), exposed: true, - value_source: ParsedValueSource::Scope(parse_quote!("graphene_std::platform_application_io::WgpuExecutorNode")), + value_source: ParsedValueSource::Scope(Box::new(parse_quote!("graphene_std::platform_application_io::WgpuExecutorNode"))), number_soft_min: None, number_soft_max: None, number_hard_min: None, @@ -287,7 +287,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) } }; @@ -305,7 +305,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(), @@ -314,7 +314,7 @@ impl PerPixelAdjustCodegen<'_> { context_features: self.parsed.input.context_features.clone(), }, output_type: raster_gpu, - 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 7b51b31efa..bc393560f9 100644 --- a/node-graph/node-macro/src/validation.rs +++ b/node-graph/node-macro/src/validation.rs @@ -11,6 +11,7 @@ pub fn validate_node_fn(parsed: &ParsedNodeFn) -> syn::Result<()> { validate_primary_input_expose, validate_min_max, validate_range_slider_bounds, + validate_async_source, ]; for validator in validators { @@ -20,6 +21,48 @@ pub fn validate_node_fn(parsed: &ParsedNodeFn) -> syn::Result<()> { Ok(()) } +fn validate_async_source(parsed: &ParsedNodeFn) { + let snapshot_ctx = matches!(&parsed.input.ty, Type::Path(path) if path.path.segments.last().is_some_and(|segment| segment.ident == "CtxSnapshot")); + let future_kernel = crate::codegen::is_source_kernel(&parsed.output_type); + if let Some(placeholder) = &parsed.attributes.placeholder + && !parsed.is_async + && !future_kernel + { + emit_error!( + placeholder.span(), + "`placeholder` applies only to async and source kernels; a synchronous node never reports `Partial`, so the stand-in is unused" + ); + } + if parsed.is_async && future_kernel { + emit_error!( + parsed.output_type.span(), + "an `async fn` kernel already is the async part; returning `SourceFuture` is the sync-prologue form, so drop the `async` keyword or return the value directly" + ); + return; + } + if !parsed.is_async { + if snapshot_ctx { + emit_error!( + parsed.input.pat_ident.span(), + "`CtxSnapshot` is the async source context; synchronous nodes take `impl Ctx` and read through extract bounds" + ); + } + if !future_kernel { + return; + } + } + if parsed.is_async { + for field in &parsed.fields { + if matches!(field.ty, ParsedFieldType::Node(_)) { + emit_error!( + field.pat_ident.span(), + "`async fn` source nodes cannot take `impl Node` inputs: the spawned future outlives any borrow of the graph, so it cannot evaluate other nodes; use the sync-prologue form (return `SourceFuture`) to evaluate lazy inputs before spawning" + ); + } + } + } +} + fn validate_min_max(parsed: &ParsedNodeFn) { for field in &parsed.fields { if let ParsedField { diff --git a/node-graph/nodes/brush/src/brush.rs b/node-graph/nodes/brush/src/brush.rs index 53b0ea0052..4ac6349e79 100644 --- a/node-graph/nodes/brush/src/brush.rs +++ b/node-graph/nodes/brush/src/brush.rs @@ -1,17 +1,14 @@ use crate::brush_cache::BrushCache; use crate::brush_stroke::{BrushStroke, BrushStyle}; +use core_types::Ctx; use core_types::blending::BlendMode; use core_types::bounds::{BoundingBox, RenderBoundingBox}; use core_types::color::{Alpha, Color, Pixel, Sample}; -use core_types::generic::FnNode; use core_types::list::{Item, List}; use core_types::math::bbox::{AxisAlignedBbox, Bbox}; -use core_types::registry::FutureWrapperNode; use core_types::transform::Transform; use core_types::uuid::NodeId; -use core_types::value::ClonedNode; use core_types::{ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM}; -use core_types::{Ctx, Node}; use glam::{DAffine2, DVec2}; use raster_nodes::blending_nodes::blend_colors; use raster_nodes::std_nodes::{empty_image, extend_image_to_bounds}; @@ -63,7 +60,7 @@ impl Sample for BrushStampGenerator

{ /// The feather exponent is calculated from hardness to determine edge softness. /// Used internally to create the brush texture before stamping it repeatedly along a stroke path. #[node_macro::node(category(""), skip_impl)] -fn brush_stamp_generator(#[unit(" px")] diameter: f64, color: Color, hardness: f64, flow: f64) -> BrushStampGenerator { +fn brush_stamp_generator(_: impl Ctx, #[unit(" px")] diameter: f64, color: Color, hardness: f64, flow: f64) -> BrushStampGenerator { // Diameter let radius = diameter / 2.; @@ -83,9 +80,9 @@ fn brush_stamp_generator(#[unit(" px")] diameter: f64, color: Color, hardness: f /// Used to efficiently paint brush strokes. Applies the same texture repeatedly at different positions with proper blending and boundary handling. #[node_macro::node(category(""), skip_impl)] -fn blit(mut target: List>, texture: Raster, positions: Vec, blend_mode: BlendFn) -> List> +fn blit(_: impl Ctx, mut target: List>, texture: Raster, positions: Vec, blend_mode: BlendFn) -> List> where - BlendFn: for<'any_input> Node<'any_input, (Color, Color), Output = Color>, + BlendFn: Fn(Color, Color) -> Color, { if positions.is_empty() { return target; @@ -125,7 +122,7 @@ where for x in blit_area_offset.x..blit_area_offset.x + blit_area_dimensions.x { let src_pixel = texture.data[texture_index(x, y)]; let dst_pixel = &mut element.data_mut().data[target_index(x + clamp_start.x, y + clamp_start.y)]; - *dst_pixel = blend_mode.eval((src_pixel, *dst_pixel)); + *dst_pixel = blend_mode(src_pixel, *dst_pixel); } } } @@ -134,10 +131,10 @@ where target } -pub async fn create_brush_texture(brush_style: &BrushStyle) -> Raster { - let stamp = brush_stamp_generator(brush_style.diameter, brush_style.color, brush_style.hardness, brush_style.flow); +pub fn create_brush_texture(brush_style: &BrushStyle) -> Raster { + let stamp = brush_stamp_generator(&(), brush_style.diameter, brush_style.color, brush_style.hardness, brush_style.flow); let transform = DAffine2::from_scale_angle_translation(DVec2::splat(brush_style.diameter), 0., -DVec2::splat(brush_style.diameter / 2.)); - let blank_texture = empty_image((), transform, List::new_from_element(Color::TRANSPARENT)).into_iter().next().unwrap_or_default(); + let blank_texture = empty_image(&(), transform, List::new_from_element(Color::TRANSPARENT)).into_iter().next().unwrap_or_default(); let image = blend_stamp_closure(stamp, blank_texture, |a, b| blend_colors(a, b, BlendMode::Normal, 1.)); image.into_element() @@ -188,7 +185,7 @@ pub fn blend_with_mode(background: Item>, foreground: Item>, @@ -224,7 +221,7 @@ async fn brush( let mut brush_plan = cache.compute_brush_plan(list_item, &draw_strokes); // TODO: Find a way to handle more than one item - let Some(mut actual_image) = extend_image_to_bounds((), List::new_from_item(brush_plan.background), background_bounds).into_iter().next() else { + let Some(mut actual_image) = extend_image_to_bounds(&(), List::new_from_item(brush_plan.background), background_bounds).into_iter().next() else { return List::new(); }; @@ -234,7 +231,7 @@ async fn brush( // TODO: apply rotation from layer to stamp for non-rotationally-symmetric brushes. let mut brush_texture = cache.get_cached_brush(&stroke.style); if brush_texture.is_none() { - let tex = create_brush_texture(&stroke.style).await; + let tex = create_brush_texture(&stroke.style); cache.store_brush(stroke.style.clone(), tex.clone()); brush_texture = Some(tex); } @@ -255,21 +252,14 @@ async fn brush( let stroke_origin_in_layer = bbox.start - snap_offset - DVec2::splat(stroke.style.diameter / 2.); let stroke_to_layer = DAffine2::from_translation(stroke_origin_in_layer) * DAffine2::from_scale(stroke_size); - let normal_blend = FnNode::new(|(a, b)| blend_colors(a, b, BlendMode::Normal, 1.)); - let blit_node = BlitNode::new( - FutureWrapperNode::new(ClonedNode::new(brush_texture)), - FutureWrapperNode::new(ClonedNode::new(positions)), - FutureWrapperNode::new(ClonedNode::new(normal_blend)), - ); let blit_target = if idx == 0 { let target = core::mem::take(&mut brush_plan.first_stroke_texture); - extend_image_to_bounds((), List::new_from_item(target), stroke_to_layer) + extend_image_to_bounds(&(), List::new_from_item(target), stroke_to_layer) } else { - empty_image((), stroke_to_layer, List::new_from_element(Color::TRANSPARENT)) - // EmptyImageNode::new(CopiedNode::new(stroke_to_layer), CopiedNode::new(Color::TRANSPARENT)).eval(()) + empty_image(&(), stroke_to_layer, List::new_from_element(Color::TRANSPARENT)) }; - let list = blit_node.eval(blit_target).await; + let list = blit(&(), blit_target, brush_texture, positions, |a, b| blend_colors(a, b, BlendMode::Normal, 1.)); assert_eq!(list.len(), 1); list.into_iter().next().unwrap_or_default() }; @@ -291,7 +281,7 @@ async fn brush( for stroke in trace.into_iter().map(|row| row.into_element()) { let mut brush_texture = cache.get_cached_brush(&stroke.style); if brush_texture.is_none() { - let tex = create_brush_texture(&stroke.style).await; + let tex = create_brush_texture(&stroke.style); cache.store_brush(stroke.style.clone(), tex.clone()); brush_texture = Some(tex); } @@ -305,17 +295,15 @@ async fn brush( _ => BlendMode::Restore, }; - let blend_params = FnNode::new(move |(a, b)| blend_colors(a, b, mask_blend_mode, 1.)); - let blit_node = BlitNode::new( - FutureWrapperNode::new(ClonedNode::new(brush_texture)), - FutureWrapperNode::new(ClonedNode::new(positions)), - FutureWrapperNode::new(ClonedNode::new(blend_params)), - ); - erase_restore_mask = blit_node.eval(List::new_from_item(erase_restore_mask)).await.into_iter().next().unwrap_or_default(); + erase_restore_mask = blit(&(), List::new_from_item(erase_restore_mask), brush_texture, positions, move |a, b| { + blend_colors(a, b, mask_blend_mode, 1.) + }) + .into_iter() + .next() + .unwrap_or_default(); } - let blend_params = FnNode::new(|(a, b)| blend_colors(a, b, BlendMode::MultiplyAlpha, 1.)); - actual_image = blend_image_closure(erase_restore_mask, actual_image, |a, b| blend_params.eval((a, b))); + actual_image = blend_image_closure(erase_restore_mask, actual_image, |a, b| blend_colors(a, b, BlendMode::MultiplyAlpha, 1.)); } let transform: DAffine2 = actual_image.attribute_cloned_or_default(ATTR_TRANSFORM); @@ -410,16 +398,16 @@ mod test { #[test] fn test_brush_texture() { let size = 20.; - let image = brush_stamp_generator(size, Color::BLACK, 100., 100.); + let image = brush_stamp_generator(&(), size, Color::BLACK, 100., 100.); assert_eq!(image.transform(), DAffine2::from_scale_angle_translation(DVec2::splat(size.ceil()), 0., -DVec2::splat(size / 2.))); // center pixel should be BLACK assert_eq!(image.sample(DVec2::splat(0.), DVec2::ONE), Some(Color::BLACK)); } - #[tokio::test] - async fn test_brush_output_size() { + #[test] + fn test_brush_output_size() { let image = brush( - (), + &(), &BrushCache::default(), List::new_from_element(Raster::new_cpu(Image::::default())), List::new_from_element(BrushStroke { @@ -433,8 +421,7 @@ mod test { blend_mode: BlendMode::Normal, }, }), - ) - .await; + ); assert_eq!(image.element(0).unwrap().width, 20); } } diff --git a/node-graph/nodes/gcore/src/animation.rs b/node-graph/nodes/gcore/src/animation.rs index 4182847d00..9bd9ef2993 100644 --- a/node-graph/nodes/gcore/src/animation.rs +++ b/node-graph/nodes/gcore/src/animation.rs @@ -1,6 +1,7 @@ +use core_types::gpoll::GPoll; use core_types::list::List; use core_types::transform::Footprint; -use core_types::{CacheHash, CloneVarArgs, Color, Context, Ctx, ExtractAll, ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime, OwnedContextImpl}; +use core_types::{CacheHash, Color, Context, Ctx, DeriveCtx, ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime}; use glam::{DAffine2, DVec2}; use graphic_types::vector_types::GradientStops; use graphic_types::{Artboard, Graphic, Vector}; @@ -61,8 +62,8 @@ fn animation_time( } #[node_macro::node(category("Debug"))] -async fn quantize_real_time( - ctx: impl Ctx + ExtractAll + CloneVarArgs, +fn quantize_real_time( + ctx: impl Ctx + ExtractRealTime + DeriveCtx, #[implementations( Context -> bool, Context -> u32, @@ -84,11 +85,11 @@ async fn quantize_real_time( Context -> List, Context -> (), )] - value: impl Node<'n, Context<'static>, Output = T>, + value: impl Node, Output = T>, #[default(1)] #[unit("sec")] quantum: f64, -) -> T { +) -> GPoll { let time = ctx.try_real_time().unwrap_or_default(); let time = time / 1000.; let mut quantized_time = (time * quantum.recip()).round() / quantum.recip(); @@ -96,13 +97,13 @@ async fn quantize_real_time( quantized_time = 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 + let scope = ctx.scope().with_real_time(Some(quantized_time)); + value.eval(&ctx.with_scope(&scope)) } #[node_macro::node(category("Debug"))] -async fn quantize_animation_time( - ctx: impl Ctx + ExtractAll + CloneVarArgs, +fn quantize_animation_time( + ctx: impl Ctx + ExtractAnimationTime + DeriveCtx, #[implementations( Context -> bool, Context -> u32, @@ -124,18 +125,18 @@ async fn quantize_animation_time( Context -> List, Context -> (), )] - value: impl Node<'n, Context<'static>, Output = T>, + value: impl Node, Output = T>, #[default(1)] #[unit("sec")] quantum: f64, -) -> T { +) -> GPoll { let time = ctx.try_animation_time().unwrap_or_default(); let mut quantized_time = (time * quantum.recip()).round() / quantum.recip(); if !quantized_time.is_finite() { quantized_time = time; } - let new_context = OwnedContextImpl::from(ctx).with_animation_time(quantized_time); - value.eval(Some(new_context.into())).await + let scope = ctx.scope().with_animation_time(Some(quantized_time)); + value.eval(&ctx.with_scope(&scope)) } /// 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 8b11d3d657..e697f5c851 100644 --- a/node-graph/nodes/gcore/src/context.rs +++ b/node-graph/nodes/gcore/src/context.rs @@ -47,7 +47,7 @@ fn read_gradient(ctx: impl Ctx + ExtractVarArgs) -> List { } #[node_macro::node(category("Context"), path(core_types::vector))] -async fn read_position( +fn read_position( ctx: impl Ctx + ExtractPosition, _primary: (), /// The number of nested loops to traverse outwards (from the innermost loop) to get the position from. The most upstream loop is level 0, and downstream loops add levels. @@ -64,7 +64,7 @@ async fn read_position( /// /// Nested loops can enable 2D or higher-dimensional iteration by using the *Loop Level* parameter to read the index from outer levels of loops. #[node_macro::node(category("Context"), path(core_types::vector))] -async fn read_index( +fn read_index( ctx: impl Ctx + ExtractIndex, _primary: (), /// The number of nested loops to traverse outwards (from the innermost loop) to get the index from. The most upstream loop is level 0, and downstream loops add levels. diff --git a/node-graph/nodes/gcore/src/context_modification.rs b/node-graph/nodes/gcore/src/context_modification.rs index aa8f72f1d4..f9338886b3 100644 --- a/node-graph/nodes/gcore/src/context_modification.rs +++ b/node-graph/nodes/gcore/src/context_modification.rs @@ -1,9 +1,10 @@ use core::f64; -use core_types::context::{CloneVarArgs, Context, ContextFeatures, Ctx, ExtractAll}; +use core_types::Color; +use core_types::context::{Context, ContextModification, Ctx, DeriveCtx}; +use core_types::gpoll::GPoll; use core_types::list::{AttributeDyn, AttributeValueDyn, List, ListDyn}; use core_types::transform::Footprint; use core_types::uuid::NodeId; -use core_types::{Color, OwnedContextImpl}; use glam::{DAffine2, DVec2}; use graphic_types::vector_types::GradientStops; use graphic_types::{Artboard, Graphic, Vector}; @@ -12,8 +13,8 @@ use raster_types::{CPU, GPU, Raster}; /// Filters out what should be unused components of the context based on the specified requirements. /// This node is inserted by the compiler to "zero out" unused context components. #[node_macro::node(category(""))] -async fn context_modification( - ctx: impl Ctx + CloneVarArgs + ExtractAll, +fn context_modification( + ctx: impl Ctx + DeriveCtx, /// The data to pass through, evaluated with the stripped down context. #[implementations( Context -> (), @@ -41,80 +42,10 @@ async fn context_modification( Context -> AttributeValueDyn, Context -> ListDyn, )] - value: impl Node, Output = T>, + 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: ContextFeatures, -) -> T { - let new_context = OwnedContextImpl::from_flags(ctx, features_to_keep); - - value.eval(Some(new_context.into())).await -} - -#[cfg(test)] -mod tests { - use super::*; - use core_types::graphene_hash::CacheHash; - use core_types::transform::Footprint; - use std::collections::hash_map::DefaultHasher; - use std::hash::Hasher; - - /// Verifies that nullified context fields don't affect the cache hash — only the kept features matter. - #[test] - fn test_nullified_context_hash_stability() { - use core_types::Context; - use std::sync::Arc; - - let original_ctx: Context = Some(Arc::new( - OwnedContextImpl::empty() - .with_footprint(Footprint::default()) - .with_index(1) - .with_real_time(10.5) - .with_vararg(Box::new("test")) - .with_animation_time(20.25), - )); - - // A second context with different values for the nullified fields - let changed_ctx: Context = Some(Arc::new( - OwnedContextImpl::empty() - .with_footprint(Footprint::default()) - .with_index(2) - .with_real_time(999.9) - .with_vararg(Box::new("test")) - .with_animation_time(888.8), - )); - - // Nullify everything — both should hash the same regardless of their field values - let features_to_keep = ContextFeatures::empty(); - let nullified1 = OwnedContextImpl::from_flags(original_ctx.clone().unwrap(), features_to_keep); - let nullified2 = OwnedContextImpl::from_flags(changed_ctx.clone().unwrap(), features_to_keep); - - let mut hasher1 = DefaultHasher::new(); - nullified1.cache_hash(&mut hasher1); - - let mut hasher2 = DefaultHasher::new(); - nullified2.cache_hash(&mut hasher2); - - assert_eq!( - hasher1.finish(), - hasher2.finish(), - "Hash of nullified context should remain stable regardless of input changes when features are nullified" - ); - - // Keep only footprint and varargs — both have the same footprint and vararg, so hash should still match - let partial_features = ContextFeatures::FOOTPRINT | ContextFeatures::VARARGS; - let partial1 = OwnedContextImpl::from_flags(original_ctx.clone().unwrap(), partial_features); - let partial2 = OwnedContextImpl::from_flags(changed_ctx.clone().unwrap(), partial_features); - - let mut hasher3 = DefaultHasher::new(); - partial1.cache_hash(&mut hasher3); - - let mut hasher4 = DefaultHasher::new(); - partial2.cache_hash(&mut hasher4); - - assert_eq!( - hasher3.finish(), - hasher4.finish(), - "Hash should be stable when keeping only footprint and varargs and their values are the same" - ); - } + modification: ContextModification, +) -> GPoll { + let scope = ctx.scope().nullified(modification.features, Some(modification.sources())); + value.eval(&ctx.nullified(modification.features, &scope)) } diff --git a/node-graph/nodes/gcore/src/memo.rs b/node-graph/nodes/gcore/src/memo.rs index cc5befe0e2..7066eb9db5 100644 --- a/node-graph/nodes/gcore/src/memo.rs +++ b/node-graph/nodes/gcore/src/memo.rs @@ -1,54 +1,267 @@ -use core_types::WasmNotSend; +use core_types::arena::{Arena, ArenaCell}; +use core_types::context::{Ctx, CtxSnapshot, DeriveCtx, ExtractAll}; +use core_types::frame_table::{FrameTable, Lookup}; +use core_types::gpoll::{Extent, Finality, GPoll, Interrupt}; use core_types::graphene_hash::CacheHash; use core_types::memo::*; -use std::hash::DefaultHasher; -use std::hash::Hasher; +use core_types::node::Node; +use core_types::registry::cache_key; use std::sync::Arc; use std::sync::Mutex; /// Helps speed up repeated renders in a computationally-heavy part of the node graph. /// /// Stores the last evaluated data that flowed through this node and immediately returns that data on subsequent renders if the context has not changed. -#[node_macro::node(category("General"), path(graphene_core::memo), skip_impl)] -async fn memoize(input: I, #[data] cache: Arc>>, content: impl Node) -> T { - // 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. - // - // A cache hit occurs when the Option is Some and has a stored hash matching the hash of the call argument. In this case, the node returns the cached value without re-evaluating the inner node. - // - // Currently, only one input-output pair is cached. Subsequent calls with different inputs will overwrite the previous cache. +#[node_macro::node(category("General"), path(graphene_core::memo), skip_impl, extent(memoize_extent))] +fn memoize(input: I, #[data] cache: Arc>>, content: impl Node) -> GPoll { + let key = cache_key(&input); + if let Some((hash, value, finality)) = cache.lock().unwrap().as_ref() + && *hash == key + { + return match finality { + Finality::AllFinal => GPoll::Final(value.clone()), + Finality::Partial => GPoll::Partial(value.clone()), + }; + } + let result = content.eval(input); + match &result { + GPoll::Final(value) => *cache.lock().unwrap() = Some((key, value.clone(), Finality::AllFinal)), + GPoll::Partial(value) => *cache.lock().unwrap() = Some((key, value.clone(), Finality::Partial)), + GPoll::Pending | GPoll::Fallback(_) | GPoll::Error(_) => {} + } + result +} - let mut hasher = DefaultHasher::new(); - input.cache_hash(&mut hasher); - let hash = hasher.finish(); +fn memoize_extent(node: &MemoizeNode, ctx: &C) -> GPoll +where + T: Clone, + NodeContent: Node, +{ + node.content.extent(ctx) +} - if let Some(data) = cache.lock().as_ref().unwrap().as_ref().and_then(|data| (data.0 == hash).then_some(data.1.clone())) { - return data; +#[node_macro::node(category(""), path(graphene_core::memo), skip_impl, extent(frame_memo_extent))] +fn frame_memo<'e, T: Clone + 'static + Send + Sync>( + ctx: impl Ctx + CacheHash + ExtractArena<'e>, + #[data] cell: ArenaCell>, + content: impl Node, Output = T>, +) -> GPoll<&'e T> { + let arena = ctx.arena(); + let table = match cell.load(arena) { + Some(table) => table, + None => match arena.alloc(FrameTable::new()) { + Some((table, weak)) => { + cell.store(weak); + table + } + None => return park(arena, content.eval(ctx)), + }, + }; + match table.lookup(cache_key(ctx)) { + Lookup::Hit(Finality::AllFinal, value) => GPoll::Final(value), + Lookup::Hit(Finality::Partial, value) => GPoll::Partial(value), + Lookup::Vacant(slot) => match content.eval(ctx) { + GPoll::Final(value) => GPoll::Final(slot.publish(value, Finality::AllFinal)), + GPoll::Partial(value) => GPoll::Partial(slot.publish(value, Finality::Partial)), + unpublishable => { + slot.release(); + park(arena, unpublishable) + } + }, + Lookup::Full => park(arena, content.eval(ctx)), } +} - let value = content.eval(input).await; - *cache.lock().unwrap() = Some((hash, value.clone())); - value +fn frame_memo_extent(node: &FrameMemoNode, ctx: &C) -> GPoll +where + T: Clone + 'static + Send + Sync, + NodeContent: Node, +{ + node.content.extent(ctx) } -type MonitorValue = Arc>>>>; +pub fn park(arena: &Arena, result: GPoll) -> GPoll<&T> { + match result { + GPoll::Final(value) => match arena.alloc(value) { + Some((parked, _)) => GPoll::Final(parked), + None => GPoll::arena_exhausted(), + }, + GPoll::Partial(value) => match arena.alloc(value) { + Some((parked, _)) => GPoll::Partial(parked), + None => GPoll::arena_exhausted(), + }, + GPoll::Fallback(boxed) => { + let (value, error) = *boxed; + match arena.alloc(value) { + Some((parked, _)) => GPoll::Fallback(Box::new((parked, error))), + None => GPoll::arena_exhausted(), + } + } + GPoll::Pending => GPoll::Pending, + GPoll::Error(error) => GPoll::Error(error), + } +} + +type MonitorValue = Arc>>>>; /// The Monitor node is used by the editor to access the data flowing through it. #[node_macro::node(category(""), path(graphene_core::memo), serialize(serialize_monitor), properties("monitor_properties"), skip_impl)] -async fn monitor( - input: I, +fn monitor( + ctx: impl Ctx + DeriveCtx + ExtractAll, #[allow(clippy::type_complexity)] #[data] - io: MonitorValue, - content: impl Node, -) -> T { - let output = content.eval(input.clone()).await; - *io.lock().unwrap() = Some(Arc::new(IORecord { input, output: output.clone() })); - output + io: MonitorValue, + content: impl Node, Output = T>, +) -> Result { + let output = content.eval(&ctx.derived())?; + *io.lock().unwrap() = Some(Arc::new(IORecord { + input: CtxSnapshot::capture(ctx), + output: output.clone(), + })); + Ok(output) } -fn serialize_monitor(io: &MonitorValue) -> Option> { +fn serialize_monitor(io: &MonitorValue) -> Option> { let io = io.lock().unwrap(); io.as_ref().map(|output| output.clone() as Arc) } + +#[cfg(test)] +mod tests { + use super::*; + use core_types::SourceId; + use core_types::context::{ContextImpl, EvalScope}; + use core_types::registry::{EdgeHandle, ErasedLendNode, ErasedNode}; + use std::sync::atomic::{AtomicU32, Ordering}; + + struct CountingNode(AtomicU32); + + impl Node for CountingNode { + type Output = u32; + + fn eval(&self, _input: &Input) -> GPoll { + GPoll::Final(self.0.fetch_add(1, Ordering::Relaxed) + 1) + } + } + + struct PartialCountingNode(AtomicU32); + + impl Node for PartialCountingNode { + type Output = u32; + + fn eval(&self, _input: &Input) -> GPoll { + GPoll::Partial(self.0.fetch_add(1, Ordering::Relaxed) + 1) + } + } + + struct ValueNode(T); + + impl Node for ValueNode { + type Output = T; + + fn eval(&self, _input: &Input) -> GPoll { + GPoll::Final(self.0.clone()) + } + } + + fn scope_fixture<'a>(generations: &'a [(SourceId, u64)], arena: &'a Arena) -> EvalScope<'a> { + EvalScope::new(Some(0.5), None, None, generations, arena) + } + + #[test] + fn monitor_serialize_exposes_the_io_record_through_the_edge() { + let arena = Arena::new(1024).unwrap(); + let generations = []; + let scope = scope_fixture(&generations, &arena); + let ctx = ContextImpl::root(&scope); + + let handle = EdgeHandle::new(Arc::new(MonitorNode::new(ValueNode(11u32))) as Arc>); + assert!(handle.serialize().is_none(), "no record before the first eval"); + + let edge = handle.duplicate().downcast::().unwrap(); + assert_eq!(edge.eval(&ctx), GPoll::Final(11)); + + let record = handle.serialize().expect("the eval landed a record"); + let record = record.downcast_ref::>().expect("the record is the monitor io"); + assert_eq!(record.output, 11); + } + + #[test] + fn memoize_caches_across_evals() { + let arena = Arena::new(1024).unwrap(); + let generations = []; + let scope = scope_fixture(&generations, &arena); + let ctx = ContextImpl::root(&scope); + + let memoized = MemoizeNode::new(CountingNode(AtomicU32::new(0))); + + assert_eq!(memoized.eval(&ctx), GPoll::Final(1)); + assert_eq!(memoized.eval(&ctx), GPoll::Final(1)); + } + + #[test] + fn memo_invalidates_on_generation_bump() { + let arena = Arena::new(1024).unwrap(); + let source: SourceId = 7; + let before = [(source, 1)]; + let after = [(source, 2)]; + let scope_before = scope_fixture(&before, &arena); + let scope_after = scope_fixture(&after, &arena); + + let memoized = MemoizeNode::new(CountingNode(AtomicU32::new(0))); + + assert_eq!(memoized.eval(&ContextImpl::root(&scope_before)), GPoll::Final(1)); + assert_eq!(memoized.eval(&ContextImpl::root(&scope_before)), GPoll::Final(1)); + assert_eq!(memoized.eval(&ContextImpl::root(&scope_after)), GPoll::Final(2)); + } + + #[test] + fn memo_replays_partiality_on_hit() { + let arena = Arena::new(1024).unwrap(); + let generations = []; + let scope = scope_fixture(&generations, &arena); + let ctx = ContextImpl::root(&scope); + + let memoized = MemoizeNode::new(PartialCountingNode(AtomicU32::new(0))); + + assert_eq!(memoized.eval(&ctx), GPoll::Partial(1)); + assert_eq!(memoized.eval(&ctx), GPoll::Partial(1)); + } + + #[test] + fn memoized_edges_stack_and_rewire() { + let arena = Arena::new(1024).unwrap(); + let generations = []; + let scope = scope_fixture(&generations, &arena); + let ctx = ContextImpl::root(&scope); + + let edge = EdgeHandle::new(Arc::new(CountingNode(AtomicU32::new(0))) as Arc>); + let memoized = EdgeHandle::new(Arc::new(MemoizeNode::new(edge.downcast::().unwrap())) as Arc>); + let stacked = MemoizeNode::new(memoized.downcast::().unwrap()); + + assert_eq!(stacked.eval(&ctx), GPoll::Final(1)); + assert_eq!(stacked.eval(&ctx), GPoll::Final(1)); + } + + #[test] + fn frame_memo_turns_an_owned_edge_into_a_lending_edge() { + let arena = Arena::new(4096).unwrap(); + let generations = []; + let scope = scope_fixture(&generations, &arena); + let ctx = ContextImpl::root(&scope); + + let edge = EdgeHandle::new(Arc::new(ValueNode("lent out".to_string())) as Arc>); + let lending = EdgeHandle::new_ref(Arc::new(FrameMemoNode::new(edge.downcast::().unwrap())) as Arc>); + assert_eq!(*lending.ty(), core_types::registry::lend_edge_type::()); + + let node = lending.downcast_lend::().unwrap(); + let GPoll::Final(first) = node.eval(&ctx) else { + panic!("lend must fill the frame table and lend"); + }; + let GPoll::Final(second) = node.eval(&ctx) else { + panic!("second eval must lend the published value"); + }; + assert_eq!(first, "lent out"); + assert!(std::ptr::eq(first, second)); + } +} diff --git a/node-graph/nodes/gcore/src/ops.rs b/node-graph/nodes/gcore/src/ops.rs index 378d73569c..f864dd41ad 100644 --- a/node-graph/nodes/gcore/src/ops.rs +++ b/node-graph/nodes/gcore/src/ops.rs @@ -1,9 +1,8 @@ -use core_types::{Ctx, ExtractFootprint, ops::Convert, transform::Footprint}; +use core_types::ExtractAll; +use core_types::runtime::SourceFuture; +use core_types::{Ctx, ops::Convert, ops::ConvertAsync, transform::Footprint}; use std::marker::PhantomData; -// Re-export TypeNode from core-types for convenience -pub use core_types::ops::TypeNode; - /// Passes-through the input value without changing it. This is useful for rerouting wires for organization purposes. #[node_macro::node(category("General"), skip_impl)] fn passthrough<'i, T: 'i + Send>(_: impl Ctx, content: T) -> T { @@ -11,13 +10,18 @@ fn passthrough<'i, T: 'i + Send>(_: impl Ctx, content: T) -> T { } #[node_macro::node(category(""), skip_impl)] -fn into<'i, T: 'i + Send + Into, O: 'i + Send>(_: impl Ctx, value: T, _out_ty: PhantomData) -> O { +fn into, O: Send>(_: impl Ctx, value: T, #[data] _out_ty: PhantomData) -> O { value.into() } #[node_macro::node(category(""), skip_impl)] -async fn convert<'i, T: 'i + Send + Convert, O: 'i + Send, C: 'i + Send>(ctx: impl Ctx + ExtractFootprint, value: T, converter: C, _out_ty: PhantomData) -> O { - value.convert(*ctx.try_footprint().unwrap_or(&Footprint::DEFAULT), converter).await +fn convert, O: Send, C: Send>(ctx: impl Ctx + ExtractAll, value: T, converter: C, #[data] _out_ty: PhantomData) -> O { + value.convert(*ctx.try_footprint().unwrap_or(&Footprint::DEFAULT), converter) +} + +#[node_macro::node(category(""), skip_impl)] +fn convert_async, O: Send + 'static, C: Send>(ctx: impl Ctx + ExtractAll, value: T, converter: C, #[data] _out_ty: PhantomData) -> SourceFuture { + value.convert(*ctx.try_footprint().unwrap_or(&Footprint::DEFAULT), converter) } #[cfg(test)] @@ -26,6 +30,6 @@ mod test { #[test] pub fn passthrough_node() { - assert_eq!(passthrough((), &4), &4); + assert_eq!(passthrough(&(), &4), &4); } } diff --git a/node-graph/nodes/graphic/src/artboard.rs b/node-graph/nodes/graphic/src/artboard.rs index 4bee951646..b93b4afc81 100644 --- a/node-graph/nodes/graphic/src/artboard.rs +++ b/node-graph/nodes/graphic/src/artboard.rs @@ -1,6 +1,7 @@ +use core_types::gpoll::Interrupt; 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, Color, Context, Ctx, DeriveCtx, ModifyFootprint}; use glam::{DAffine2, DVec2}; use graphic_types::graphic::{Graphic, IntoGraphicList}; use graphic_types::{Artboard, Vector}; @@ -9,8 +10,8 @@ use vector_types::GradientStops; /// Constructs a single-element `Artboard[]` with the given content and metadata stored as row attributes. #[node_macro::node(category(""))] -pub async fn create_artboard( - ctx: impl ExtractAll + CloneVarArgs + Ctx, +pub fn create_artboard( + ctx: impl Ctx + DeriveCtx + ModifyFootprint, /// Graphics to include within the artboard. #[implementations( Context -> List, @@ -22,7 +23,7 @@ pub async fn create_artboard( Context -> List, Context -> DAffine2, )] - content: impl Node, Output = T>, + content: impl Node, Output = T>, /// Coordinate of the top-left corner of the artboard within the document. location: DVec2, /// Width and height of the artboard within the document. @@ -32,14 +33,9 @@ pub async fn create_artboard( /// Whether to cut off the contained content that extends outside the artboard, or keep it visible. #[default(true)] clip: bool, -) -> List { - let footprint = ctx.try_footprint().copied(); - let mut new_ctx = OwnedContextImpl::from(ctx); - if let Some(mut footprint) = footprint { - footprint.translate(location); - new_ctx = new_ctx.with_footprint(footprint); - } - let content = content.eval(new_ctx.into_context()).await.into_graphic_list(); +) -> Result, Interrupt> { + let translated = ctx.modify_footprint(|footprint| footprint.translate(location)); + let content = content.eval(&translated.ctx())?.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 @@ -50,11 +46,11 @@ pub async fn create_artboard( let background = background.element(0).copied().unwrap_or(Color::WHITE); // Name is not stored here, it's resolved live from the parent layer's display name - List::new_from_item( + Ok(List::new_from_item( 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), - ) + )) } diff --git a/node-graph/nodes/graphic/src/graphic.rs b/node-graph/nodes/graphic/src/graphic.rs index 93ab6a4079..67ae51f400 100644 --- a/node-graph/nodes/graphic/src/graphic.rs +++ b/node-graph/nodes/graphic/src/graphic.rs @@ -1,8 +1,9 @@ use core_types::bounds::{BoundingBox, RenderBoundingBox}; +use core_types::gpoll::Interrupt; use core_types::list::{AttributeDyn, AttributeValueDyn, Item, List, ListDyn}; use core_types::registry::types::{Angle, SignedInteger}; use core_types::uuid::NodeId; -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, Color, Context, Ctx, DeriveCtx}; use glam::{DAffine2, DVec2}; use graphic_types::graphic::{Graphic, IntoGraphicList}; use graphic_types::{Artboard, Vector}; @@ -108,8 +109,8 @@ pub fn extract_element( } #[node_macro::node(category("General"))] -async fn map( - ctx: impl Ctx + CloneVarArgs + ExtractAll, +fn map( + ctx: impl Ctx + DeriveCtx, #[implementations( List, List, @@ -127,23 +128,24 @@ async fn map( Context -> List, Context -> List, )] - mapped: impl Node, Output = List>, -) -> List { + mapped: impl Node, Output = List>, +) -> Result, Interrupt> { + let spilled = ctx.index_head(); 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(List::new_from_item(row))).with_index(i); - let list = mapped.eval(owned_ctx.into_context()).await; + let item = List::new_from_item(row); + let scoped = ctx.push_vararg(&item); + let list = mapped.eval(&scoped.ctx().promoted(&spilled, i as u64))?; rows.extend(list); } - rows + Ok(rows) } #[node_macro::node(category("General"))] -async fn mirror( +fn mirror( _: impl Ctx, #[implementations( List, @@ -229,8 +231,8 @@ pub fn path_of_subgraph(_: impl Ctx, node_path: List) -> List { /// The value is type-erased into an `AttributeValueDyn` by an auto-inserted convert node, so this node only /// monomorphizes over `T` instead of the cartesian product `(T, U)`. #[node_macro::node(category("Attributes: Write"))] -async fn write_attribute( - ctx: impl ExtractAll + CloneVarArgs + Ctx, +fn write_attribute( + ctx: impl Ctx + DeriveCtx, /// The `List` to set the named attribute on (one value per item). #[implementations( List, @@ -252,15 +254,17 @@ async fn write_attribute( name: String, /// The node that produces the attribute value for each item. Called once per item with the item's index in context. #[implementations(Context -> AttributeValueDyn)] - value: impl Node<'n, Context<'static>, Output = AttributeValueDyn>, -) -> List { + value: impl Node, Output = AttributeValueDyn>, +) -> Result, Interrupt> { + let spilled = ctx.index_head(); 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(List::new_from_item(row))).with_index(index); - let v = value.eval(owned_ctx.into_context()).await; + let item = List::new_from_item(row); + let scoped = ctx.push_vararg(&item); + let v = value.eval(&scoped.ctx().promoted(&spilled, index as u64))?; content.set_attribute_value_dyn(&name, index, v); } - content + Ok(content) } /// Sets a named attribute on the primary list, with each value taken from the corresponding item's element in the source list (paired by index, wrapping if the source has fewer items). @@ -497,7 +501,7 @@ fn read_attribute_raster( /// Joins two `List`s of the same type, extending the base `List` with the items from the new `List`. #[node_macro::node(category("General"))] -pub async fn extend( +pub fn extend( _: impl Ctx, /// The `List` whose items will appear at the start of the extended `List`. #[implementations(List, List, List, List, List>, List>, List, List)] @@ -517,7 +521,7 @@ pub async fn extend( /// Performs an obsolete function as part of a migration from an older document format. /// Users are advised to delete this node and replace it with a new one. #[node_macro::node(category(""))] -pub async fn legacy_layer_extend( +pub fn legacy_layer_extend( _: impl Ctx, #[implementations(List, List, List, List, List>, List>, List, List)] base: List, #[expose] @@ -544,7 +548,7 @@ pub async fn legacy_layer_extend( /// Nests the input graphical content in a wrapper graphic. This essentially "groups" the input. /// The inverse of this node is 'Flatten Graphic'. #[node_macro::node(category("General"))] -pub async fn wrap_graphic + 'n>( +pub fn wrap_graphic>( _: impl Ctx, #[implementations( List, @@ -565,7 +569,7 @@ pub async fn wrap_graphic + 'n>( /// Converts a list of graphical content into a `Graphic[]` by placing it into an element of a new wrapper `Graphic[]`. /// If it is already a `Graphic[]`, it is not wrapped again. Use the 'Wrap Graphic' node if wrapping is always desired. #[node_macro::node(category("General"))] -pub async fn to_graphic( +pub fn to_graphic( _: impl Ctx, #[implementations( List, @@ -583,7 +587,7 @@ pub async fn to_graphic( /// Removes a level of nesting from a `Graphic[]`, or all nesting if "Fully Flatten" is enabled. #[node_macro::node(category("General"))] -pub async fn flatten_graphic(_: impl Ctx, content: List, fully_flatten: bool) -> List { +pub fn flatten_graphic(_: impl Ctx, content: List, fully_flatten: bool) -> List { // TODO: Avoid mutable reference, instead return a new List? fn flatten_list(output_graphic_list: &mut List, current_graphic_list: List, fully_flatten: bool, recursion_depth: usize) { for index in 0..current_graphic_list.len() { @@ -620,7 +624,7 @@ pub async fn flatten_graphic(_: impl Ctx, content: List, fully_flatten: /// Converts a `Graphic[]` into a `Vector[]` by deeply flattening any vector content it contains, and discarding any non-vector content. #[node_macro::node(category("Vector"))] -pub async fn flatten_vector(_: impl Ctx, #[implementations(List, List)] content: T) -> List { +pub fn flatten_vector(_: impl Ctx, #[implementations(List, List)] content: T) -> List { let graphic_list = content.into_graphic_list(); let mut output: List = graphic_list.clone().into_flattened_list(); @@ -653,19 +657,19 @@ pub async fn flatten_vector(_: impl Ctx, #[implementations(L /// Converts a `Graphic[]` into a `Raster[]` by deeply flattening any raster content it contains, and discarding any non-raster content. #[node_macro::node(category("Raster"))] -pub async fn flatten_raster(_: impl Ctx, #[implementations(List, List>)] content: T) -> List> { +pub fn flatten_raster(_: impl Ctx, #[implementations(List, List>)] content: T) -> List> { content.into_flattened_list() } /// Converts a `Graphic[]` into a `Color[]` by deeply flattening any color content it contains, and discarding any non-color content. #[node_macro::node(category("General"))] -pub async fn flatten_color(_: impl Ctx, #[implementations(List, List)] content: T) -> List { +pub fn flatten_color(_: impl Ctx, #[implementations(List, List)] content: T) -> List { content.into_flattened_list() } /// Converts a `Graphic[]` into a `GradientStops[]` by deeply flattening any gradient content it contains, and discarding any non-gradient content. #[node_macro::node(category("General"))] -pub async fn flatten_gradient(_: impl Ctx, #[implementations(List, List)] content: T) -> List { +pub fn flatten_gradient(_: impl Ctx, #[implementations(List, List)] content: T) -> List { content.into_flattened_list() } diff --git a/node-graph/nodes/gstd/src/any.rs b/node-graph/nodes/gstd/src/any.rs deleted file mode 100644 index c8959d23e2..0000000000 --- a/node-graph/nodes/gstd/src/any.rs +++ /dev/null @@ -1,27 +0,0 @@ -use core_types::NodeIO; -use core_types::WasmNotSend; -pub use core_types::registry::{DowncastBothNode, DynAnyNode, FutureWrapperNode, 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}; - -pub trait IntoTypeErasedNode<'n> { - fn into_type_erased(self) -> TypeErasedBox<'n>; -} - -impl<'n, N: 'n> IntoTypeErasedNode<'n> for N -where - N: for<'i> NodeIO<'i, Any<'i>, Output = FutureAny<'i>> + Sync + WasmNotSend, -{ - fn into_type_erased(self) -> TypeErasedBox<'n> { - Box::new(self) - } -} - -pub fn input_node(n: SharedNodeContainer) -> DowncastBothNode<(), O> { - downcast_node(n) -} -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 7236bb8c63..67dafe89db 100644 --- a/node-graph/nodes/gstd/src/lib.rs +++ b/node-graph/nodes/gstd/src/lib.rs @@ -1,9 +1,9 @@ -pub mod any; pub mod platform_application_io; pub mod render_background; pub mod render_cache; pub mod render_node; pub mod render_pixel_preview; +pub mod runtime; pub mod text; pub use blending_nodes; pub use brush_nodes as brush; diff --git a/node-graph/nodes/gstd/src/platform_application_io.rs b/node-graph/nodes/gstd/src/platform_application_io.rs index d71f48aa52..871add45b6 100644 --- a/node-graph/nodes/gstd/src/platform_application_io.rs +++ b/node-graph/nodes/gstd/src/platform_application_io.rs @@ -3,9 +3,11 @@ use base64::Engine; #[cfg(target_family = "wasm")] use canvas_utils::{Canvas, CanvasHandle}; use core_types::color::SRGBA8; +use core_types::gpoll::GPoll; use core_types::list::{Item, List}; #[cfg(target_family = "wasm")] use core_types::math::bbox::Bbox; +use core_types::runtime::SourceFuture; #[cfg(target_family = "wasm")] use core_types::transform::Footprint; #[cfg(target_family = "wasm")] @@ -137,7 +139,7 @@ fn image_to_bytes(_: impl Ctx, image: List>) -> List { /// Loads binary from URLs and local asset paths. Returns a transparent placeholder if the resource fails to load, allowing rendering to continue. #[node_macro::node(category("Web Request"))] -async fn load_resource<'a: 'n>(_: impl Ctx, _primary: (), #[name("URL")] url: String) -> Arc<[u8]> { +async fn load_resource(_: impl Ctx, _primary: (), #[name("URL")] url: String) -> Arc<[u8]> { let placeholder = || -> Arc<[u8]> { Arc::from(Vec::::new()) }; let response = match reqwest::Client::new().get(&url).send().await { @@ -185,14 +187,14 @@ fn decode_image(_: impl Ctx, data: Arc<[u8]>) -> List> { #[cfg(target_family = "wasm")] #[node_macro::node(category(""))] -async fn create_canvas(_: impl Ctx) -> CanvasHandle { +fn create_canvas(_: impl Ctx) -> CanvasHandle { CanvasHandle::new() } /// Renders a view of the input graphic within an area defined by the *Footprint*. #[cfg(target_family = "wasm")] #[node_macro::node(category(""))] -async fn rasterize( +async fn rasterize( _: impl Ctx, #[implementations( List, @@ -262,29 +264,37 @@ where } #[node_macro::node(category(""), inject_scope)] -pub async fn editor_api<'a: 'n>(_: impl Ctx, #[scope("editor-api")] editor_api: &'a PlatformEditorApi) -> &'a PlatformEditorApi { +pub fn editor_api(_: impl Ctx, #[scope("editor-api")] editor_api: Arc) -> Arc { editor_api } #[node_macro::node(category(""))] -pub async fn resource<'a: 'n>(_: impl Ctx, hash: ResourceHash, #[scope(editor_api::IDENTIFIER)] editor_api: &'a PlatformEditorApi) -> Resource { - let application_io = editor_api.application_io.as_ref().expect("ApplicationIo must be available when using resources"); - application_io.load_resource(hash).await.unwrap_or_else(|| { - panic!("Resource {hash} not found"); +pub fn resource(_: impl Ctx, hash: ResourceHash, #[scope(editor_api::IDENTIFIER)] editor_api: Arc) -> SourceFuture> { + let application_io = editor_api.application_io.clone(); + Box::pin(async move { + let Some(application_io) = application_io else { + return GPoll::error("ApplicationIo not available"); + }; + match application_io.load_resource(hash).await { + Some(resource) => GPoll::Final(resource), + None => GPoll::error("resource not found"), + } }) } #[node_macro::node(category(""), inject_scope)] -pub async fn wgpu_executor<'a: 'n>(_: impl Ctx, #[scope(editor_api::IDENTIFIER)] editor_api: &'a PlatformEditorApi) -> &'a ::wgpu_executor::WgpuExecutor { - editor_api - .application_io - .as_ref() - .expect("ApplicationIo not not available") - .gpu_executor() - .expect("GPU executor not available") +pub fn wgpu_executor(_: impl Ctx, #[scope(editor_api::IDENTIFIER)] editor_api: Arc) -> ::wgpu_executor::WgpuExecutorHandle { + ::wgpu_executor::WgpuExecutorHandle( + editor_api + .application_io + .as_ref() + .expect("ApplicationIo not not available") + .gpu_executor_arc() + .expect("GPU executor not available"), + ) } #[node_macro::node(category(""), inject_scope)] -pub async fn try_wgpu_executor<'a: 'n>(_: impl Ctx, #[scope(editor_api::IDENTIFIER)] editor_api: &'a PlatformEditorApi) -> Option<&'a ::wgpu_executor::WgpuExecutor> { - editor_api.application_io.as_ref()?.gpu_executor() +pub fn try_wgpu_executor(_: impl Ctx, #[scope(editor_api::IDENTIFIER)] editor_api: Arc) -> Option<::wgpu_executor::WgpuExecutorHandle> { + editor_api.application_io.as_ref()?.gpu_executor_arc().map(::wgpu_executor::WgpuExecutorHandle) } diff --git a/node-graph/nodes/gstd/src/render_background.rs b/node-graph/nodes/gstd/src/render_background.rs index 8b239a47f1..ce4a86b2f5 100644 --- a/node-graph/nodes/gstd/src/render_background.rs +++ b/node-graph/nodes/gstd/src/render_background.rs @@ -9,14 +9,10 @@ use graphic_types::raster_types::Texture; use rendering::{RenderParams, SvgRender, SvgRenderOutput}; use std::fmt::Write; use wgpu::util::DeviceExt; -use wgpu_executor::{AsyncWgpuPipeline, WgpuExecutor, WgpuPipelineCache}; +use wgpu_executor::{WgpuExecutor, WgpuPipeline, WgpuPipelineCache}; #[node_macro::node(category(""))] -async fn render_background<'a: 'n>( - ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, - #[scope(composite_background_pipeline::IDENTIFIER)] pipeline: WgpuPipelineCache, - data: RenderOutput, -) -> RenderOutput { +fn render_background<'a>(ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, #[scope(composite_background_pipeline::IDENTIFIER)] pipeline: WgpuPipelineCache, data: RenderOutput) -> RenderOutput { let footprint = ctx.footprint(); let render_params = ctx .vararg(0) @@ -35,14 +31,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 - .run::(&CompositeBackgroundArgs { - foreground: foreground_texture.as_ref(), - backgrounds: &metadata.backgrounds, - document_to_screen: doc_to_screen, - zoom: render_params.viewport_zoom.to_f32(), - }) - .await; + let blended = pipeline.run::(&CompositeBackgroundArgs { + foreground: foreground_texture.as_ref(), + backgrounds: &metadata.backgrounds, + document_to_screen: doc_to_screen, + zoom: render_params.viewport_zoom.to_f32(), + }); RenderOutputType::Texture(blended) } @@ -121,9 +115,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: Option<&'a WgpuExecutor>, + #[scope(crate::platform_application_io::try_wgpu_executor::IDENTIFIER)] executor: Option, #[data] pipeline: WgpuPipelineCache, ) -> WgpuPipelineCache { if let Some(executor) = executor { @@ -148,7 +142,7 @@ pub struct CompositeBackgroundArgs<'a> { zoom: f32, } -impl AsyncWgpuPipeline for CompositeBackground { +impl WgpuPipeline for CompositeBackground { type Args<'a> = CompositeBackgroundArgs<'a>; type Out = Texture; @@ -331,7 +325,7 @@ impl AsyncWgpuPipeline for CompositeBackground { } } - async fn run<'a>(&'a self, executor: &'a WgpuExecutor, args: &'a Self::Args<'_>) -> Self::Out { + fn run<'a>(&'a self, executor: &'a WgpuExecutor, args: &'a Self::Args<'_>) -> Self::Out { let &CompositeBackgroundArgs { foreground, backgrounds, @@ -340,7 +334,7 @@ impl AsyncWgpuPipeline for CompositeBackground { } = args; let foreground_size = foreground.size(); - let output = executor.request_texture(UVec2::new(foreground_size.width, foreground_size.height)).await; + let output = executor.request_texture(UVec2::new(foreground_size.width, foreground_size.height)); if zoom <= 0. { return output; diff --git a/node-graph/nodes/gstd/src/render_cache.rs b/node-graph/nodes/gstd/src/render_cache.rs index 43e84aab2b..304c570e98 100644 --- a/node-graph/nodes/gstd/src/render_cache.rs +++ b/node-graph/nodes/gstd/src/render_cache.rs @@ -1,8 +1,9 @@ //! Tile-based render caching for efficient viewport panning. +use core_types::gpoll::Interrupt; 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::{Ctx, DeriveCtx, ExtractAll}; use glam::{DAffine2, DVec2, IVec2, UVec2}; use graph_craft::application_io::PlatformEditorApi; use graph_craft::document::value::{RenderOutput, RenderOutputType}; @@ -11,7 +12,6 @@ use rendering::{RenderOutputType as RenderOutputTypeRequest, RenderParams}; use std::collections::HashSet; use std::hash::Hash; use std::sync::{Arc, Mutex}; -use wgpu_executor::WgpuExecutor; pub const TILE_SIZE: u32 = 256; pub const MAX_CACHE_MEMORY_BYTES: usize = 512 * 1024 * 1024; @@ -321,25 +321,23 @@ fn flood_fill(start: &TileCoord, tile_set: &HashSet, visited: &mut Ha } #[node_macro::node(category(""))] -pub async fn render_output_cache<'a: 'n>( - ctx: impl Ctx + ExtractAll + CloneVarArgs + ExtractRealTime + ExtractAnimationTime + ExtractPointerPosition + Sync, - #[scope(crate::platform_application_io::try_wgpu_executor::IDENTIFIER)] executor: Option<&'a WgpuExecutor>, - #[scope(crate::platform_application_io::editor_api::IDENTIFIER)] editor_api: &'a PlatformEditorApi, - data: impl Node, Output = RenderOutput> + Send + Sync, +pub fn render_output_cache( + ctx: impl Ctx + ExtractAll + DeriveCtx, + #[scope(crate::platform_application_io::try_wgpu_executor::IDENTIFIER)] executor: Option, + #[scope(crate::platform_application_io::editor_api::IDENTIFIER)] editor_api: std::sync::Arc, + data: impl Node, Output = RenderOutput>, #[data] tile_cache: TileCache, -) -> RenderOutput { - let footprint = ctx.footprint(); +) -> Result { + let footprint = *ctx.footprint(); let Some(render_params) = ctx.vararg(0).ok().and_then(|v| v.downcast_ref::()) else { log::warn!("render_output_cache: missing or invalid render params, falling back to direct render"); - let context = OwnedContextImpl::from(ctx.clone()).with_footprint(*footprint); - return data.eval(context.into_context()).await; + return data.eval(&ctx.derived()); }; // Fall back to direct render for non-Vello or zero-size viewports let physical_resolution = footprint.resolution; if !matches!(render_params.render_output_type, RenderOutputTypeRequest::Vello) || physical_resolution.x == 0 || physical_resolution.y == 0 { - let context = OwnedContextImpl::from(ctx.clone()).with_footprint(*footprint).with_vararg(Box::new(render_params.clone())); - return data.eval(context.into_context()).await; + return data.eval(&ctx.derived()); } let zoom = footprint.scale_magnitudes().x; @@ -375,8 +373,38 @@ pub async fn render_output_cache<'a: 'n>( if missing_region.tiles.is_empty() { continue; } - let region = render_missing_region(missing_region, |ctx| data.eval(ctx), ctx.clone(), render_params, &footprint.transform, &device_origin_offset).await; - new_regions.push(region); + let min_tile = missing_region.tiles.iter().fold(IVec2::new(i32::MAX, i32::MAX), |acc, t| acc.min(IVec2::new(t.x, t.y))); + let max_tile = missing_region.tiles.iter().fold(IVec2::new(i32::MIN, i32::MIN), |acc, t| acc.max(IVec2::new(t.x, t.y))); + + let tile_count = (max_tile - min_tile) + IVec2::ONE; + let region_pixel_size = (tile_count * TILE_SIZE as i32).as_uvec2(); + + let tile_global_offset = min_tile.as_dvec2() * TILE_SIZE as f64 + device_origin_offset; + let region_transform = DAffine2::from_translation(-tile_global_offset) * footprint.transform; + let region_footprint = Footprint { + transform: region_transform, + resolution: region_pixel_size, + quality: RenderQuality::Full, + }; + + let mut result = data.eval(&ctx.with_footprint(®ion_footprint))?; + + let RenderOutputType::Texture(texture) = result.data else { + unreachable!("render_output_cache: expected texture output from Vello render"); + }; + + result.metadata.apply_transform(region_transform.inverse()); + + let memory_size = (region_pixel_size.x * region_pixel_size.y) as usize * BYTES_PER_PIXEL; + + new_regions.push(CachedRegion { + texture, + texture_size: region_pixel_size, + tiles: missing_region.tiles.clone(), + metadata: result.metadata, + last_access: 0, + memory_size, + }); } tile_cache.store_regions(new_regions.clone()); @@ -385,68 +413,18 @@ pub async fn render_output_cache<'a: 'n>( // If no regions, fall back to direct render if all_regions.is_empty() { - let context = OwnedContextImpl::from(ctx.clone()).with_footprint(*footprint).with_vararg(Box::new(render_params.clone())); - return data.eval(context.into_context()).await; + return data.eval(&ctx.derived()); } let executor = executor.expect("GPU executor not available"); - let output_texture = executor.request_texture(physical_resolution).await; + 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); - RenderOutput { + Ok(RenderOutput { data: RenderOutputType::Texture(output_texture), metadata: combined_metadata, - } -} - -async fn render_missing_region( - region: &RenderRegion, - render_fn: F, - ctx: impl Ctx + ExtractAll + CloneVarArgs, - render_params: &RenderParams, - viewport_transform: &DAffine2, - viewport_origin_offset: &DVec2, -) -> CachedRegion -where - F: Fn(Context<'static>) -> Fut, - Fut: std::future::Future, -{ - 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))); - - let tile_count = (max_tile - min_tile) + IVec2::ONE; - let region_pixel_size = (tile_count * TILE_SIZE as i32).as_uvec2(); - - let tile_global_offset = min_tile.as_dvec2() * TILE_SIZE as f64 + *viewport_origin_offset; - let region_transform = DAffine2::from_translation(-tile_global_offset) * *viewport_transform; - let region_footprint = Footprint { - transform: region_transform, - resolution: region_pixel_size, - quality: RenderQuality::Full, - }; - - 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; - - 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; - - CachedRegion { - texture, - texture_size: region_pixel_size, - tiles: region.tiles.clone(), - metadata: result.metadata, - last_access: 0, - memory_size, - } + }) } fn composite_cached_regions( diff --git a/node-graph/nodes/gstd/src/render_node.rs b/node-graph/nodes/gstd/src/render_node.rs index dcd65a368a..2b9cef9e15 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::gpoll::Interrupt; use core_types::list::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, DeriveCtx, ExtractFootprint, ExtractVarArgs, VarArgLink, VarArgSlots, WasmNotSend}; use graph_craft::document::value::{RenderOutput, RenderOutputType}; use graphene_application_io::{ExportFormat, RenderConfig}; use graphic_types::raster_types::{CPU, Raster}; @@ -9,7 +9,7 @@ use graphic_types::{Artboard, Graphic, Vector}; use rendering::{Render, RenderMetadata, RenderOutputType as RenderOutputTypeRequest, RenderParams, SvgRender, SvgRenderOutput}; use std::sync::Arc; use vector_types::GradientStops; -use wgpu_executor::{RenderContext, WgpuExecutor}; +use wgpu_executor::RenderContext; #[derive(Clone, dyn_any::DynAny)] pub enum RenderIntermediateType { @@ -23,8 +23,8 @@ pub struct RenderIntermediate { } #[node_macro::node(category(""))] -async fn render_intermediate<'a: 'n, T: 'static + Render + WasmNotSend + Send + Sync>( - ctx: impl Ctx + ExtractVarArgs + ExtractAll + CloneVarArgs, +fn render_intermediate( + ctx: impl Ctx + ExtractVarArgs + DeriveCtx, #[implementations( Context -> List, Context -> List, @@ -34,21 +34,19 @@ async fn render_intermediate<'a: 'n, T: 'static + Render + WasmNotSend + Send + Context -> List, Context -> List, )] - data: impl Node, Output = T>, -) -> RenderIntermediate { + data: impl Node, Output = T>, +) -> Result { + let data = data.eval(&ctx.derived())?; let render_params = ctx .vararg(0) .expect("Did not find var args") .downcast_ref::() .expect("Downcasting render params yielded invalid type"); - let ctx = OwnedContextImpl::from(ctx.clone()).into_context(); - let data = data.eval(ctx).await; - let footprint = Footprint::default(); let mut metadata = RenderMetadata::default(); data.collect_metadata(&mut metadata, footprint, None); - match &render_params.render_output_type { + Ok(match &render_params.render_output_type { RenderOutputTypeRequest::Vello => { let mut scene = vello::Scene::new(); @@ -70,13 +68,13 @@ async fn render_intermediate<'a: 'n, T: 'static + Render + WasmNotSend + Send + metadata, } } - } + }) } #[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: Option<&'a WgpuExecutor>, + #[scope(crate::platform_application_io::try_wgpu_executor::IDENTIFIER)] executor: Option, data: RenderIntermediate, ) -> RenderOutput { let footprint = ctx.footprint(); @@ -133,7 +131,6 @@ async fn render<'a: 'n>( let texture = executor .expect("GPU executor not available") .render_vello_scene(&transformed_scene, footprint.resolution, context, None) - .await .expect("Failed to render Vello scene"); RenderOutputType::Texture(texture) } @@ -144,11 +141,13 @@ async fn render<'a: 'n>( } #[node_macro::node(category(""))] -async fn create_context<'a: 'n>( - // Context injections are defined in the wrap_network_in_scope function - render_config: RenderConfig, - data: impl Node, Output = RenderOutput>, -) -> RenderOutput { +fn create_context(ctx: impl Ctx + ExtractVarArgs + DeriveCtx, data: impl Node, Output = RenderOutput>) -> Result { + let render_config = *ctx + .vararg(0) + .expect("Did not find var args") + .downcast_ref::() + .expect("Downcasting render config yielded invalid type"); + let render_output_type = match render_config.export_format { ExportFormat::Svg => RenderOutputTypeRequest::Svg, ExportFormat::Raster => RenderOutputTypeRequest::Vello, @@ -169,16 +168,89 @@ async fn create_context<'a: 'n>( ..Default::default() }; - let ctx = OwnedContextImpl::default() - .with_footprint(footprint) - .with_real_time(render_config.time.time) - .with_animation_time(render_config.time.animation_time.as_secs_f64()) - .with_pointer_position(render_config.pointer) - .with_vararg(Box::new(render_params)) - .into_context(); - - let mut result = data.eval(ctx).await; + let scope = ctx + .scope() + .with_real_time(Some(render_config.time.time)) + .with_animation_time(Some(render_config.time.animation_time.as_secs_f64())) + .with_pointer_position(Some(render_config.pointer)); + let varargs = VarArgLink { + args: VarArgSlots::Single(&render_params), + outer: None, + }; + let scoped = ctx.with_scope(&scope); + let with_params = scoped.with_varargs(&varargs); + let mut result = data.eval(&with_params.with_footprint(&footprint))?; result.metadata.apply_transform(glam::DAffine2::from_scale(glam::DVec2::splat(1. / render_config.scale))); - result + Ok(result) +} + +#[cfg(test)] +mod tests { + use super::*; + use core_types::arena::Arena; + use core_types::context::{ContextImpl, EvalScope, VarArgsResult}; + use core_types::gpoll::GPoll; + use core_types::node::Node; + use core_types::{ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime}; + use graphene_application_io::TimingInformation; + + struct ProbeNode; + + impl<'a> Node> for ProbeNode { + type Output = RenderOutput; + + fn eval(&self, ctx: &ContextImpl<'a>) -> GPoll { + let render_params = ctx.vararg(0).unwrap().downcast_ref::().expect("the vararg chain must start with RenderParams"); + assert_eq!(render_params.scale, 2.0); + assert!(matches!(ctx.vararg(1), Err(VarArgsResult::IndexOutOfBounds)), "the RenderConfig must not leak downstream"); + assert_eq!(ctx.footprint().transform, glam::DAffine2::from_scale(glam::DVec2::splat(2.0)) * Footprint::DEFAULT.transform); + assert_eq!(ctx.try_real_time(), Some(1.5)); + assert_eq!(ctx.try_animation_time(), Some(2.0)); + assert_eq!(ctx.try_pointer_position(), Some(glam::DVec2::new(3.0, 4.0))); + GPoll::Final(RenderOutput { + data: RenderOutputType::Buffer { + data: Vec::new(), + width: 0, + height: 0, + }, + metadata: RenderMetadata::default(), + }) + } + } + + #[test] + fn create_context_builds_the_render_context_from_the_root_vararg() { + let arena = Arena::new(256).unwrap(); + let generations = []; + let scope = EvalScope::new(None, None, None, &generations, &arena); + let root = ContextImpl::root(&scope); + let render_config = RenderConfig { + scale: 2.0, + time: TimingInformation { + time: 1.5, + animation_time: std::time::Duration::from_secs(2), + }, + pointer: glam::DVec2::new(3.0, 4.0), + ..Default::default() + }; + let varargs = VarArgLink { + args: VarArgSlots::Single(&render_config), + outer: None, + }; + let ctx = root.with_varargs(&varargs); + + let graph = CreateContextNode::new(ProbeNode); + let GPoll::Final(result) = as Node>::eval(&graph, &ctx) else { + panic!("create_context must complete synchronously"); + }; + assert_eq!( + result.data, + RenderOutputType::Buffer { + data: Vec::new(), + width: 0, + height: 0 + } + ); + } } diff --git a/node-graph/nodes/gstd/src/render_pixel_preview.rs b/node-graph/nodes/gstd/src/render_pixel_preview.rs index f668ed8a37..4cd0910675 100644 --- a/node-graph/nodes/gstd/src/render_pixel_preview.rs +++ b/node-graph/nodes/gstd/src/render_pixel_preview.rs @@ -1,22 +1,22 @@ +use core_types::gpoll::Interrupt; use core_types::transform::{Footprint, Transform}; -use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, OwnedContextImpl}; +use core_types::{Ctx, DeriveCtx, ExtractAll}; 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>( - ctx: impl Ctx + ExtractAll + CloneVarArgs + Sync, +pub fn render_pixel_preview( + ctx: impl Ctx + ExtractAll + DeriveCtx, #[scope(pixel_preview_pipeline::IDENTIFIER)] pipeline: WgpuPipelineCache, - data: impl Node, Output = RenderOutput> + Send + Sync, -) -> RenderOutput { + data: impl Node, Output = RenderOutput>, +) -> Result { 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(&ctx.derived()); }; let physical_scale = render_params.scale; @@ -24,8 +24,7 @@ pub async fn render_pixel_preview<'a: 'n>( let viewport_zoom = footprint.scale_magnitudes().x; 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(&ctx.derived()); } let physical_resolution = footprint.resolution; @@ -51,33 +50,31 @@ pub async fn render_pixel_preview<'a: 'n>( quality: footprint.quality, }; - 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; + let scoped = ctx.push_vararg(&render_params); + let mut result = data.eval(&scoped.ctx().with_footprint(&upstream_footprint))?; - let RenderOutputType::Texture(ref source_texture) = result.data else { return result }; + let RenderOutputType::Texture(ref source_texture) = result.data else { return Ok(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 - .run::(&PixelPreviewArgs { - source: source_texture.as_ref(), - transform: &transform, - size: physical_resolution, - }) - .await; + let resampled = pipeline.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)); - result + Ok(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: Option<&'a WgpuExecutor>, + #[scope(crate::platform_application_io::try_wgpu_executor::IDENTIFIER)] executor: Option, #[data] pipeline: WgpuPipelineCache, ) -> WgpuPipelineCache { if let Some(executor) = executor { @@ -97,7 +94,7 @@ pub struct PixelPreviewArgs<'a> { size: UVec2, } -impl AsyncWgpuPipeline for PixelPreview { +impl WgpuPipeline for PixelPreview { type Args<'a> = PixelPreviewArgs<'a>; type Out = Texture; @@ -169,11 +166,11 @@ 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<'a>(&'a self, executor: &'a WgpuExecutor, args: &'a Self::Args<'_>) -> Self::Out { let context = &executor.context(); let &PixelPreviewArgs { source, transform, size } = args; - let output = executor.request_texture(size).await; + let output = executor.request_texture(size); let source_view = source.create_view(&wgpu::TextureViewDescriptor::default()); let output_view = output.create_view(&wgpu::TextureViewDescriptor::default()); diff --git a/node-graph/nodes/gstd/src/runtime.rs b/node-graph/nodes/gstd/src/runtime.rs new file mode 100644 index 0000000000..0d961b034c --- /dev/null +++ b/node-graph/nodes/gstd/src/runtime.rs @@ -0,0 +1,11 @@ +pub use core_types::runtime::*; + +use crate::platform_application_io::editor_api; +use core_types::Ctx; +use graph_craft::application_io::PlatformEditorApi; +use std::sync::Arc; + +#[node_macro::node(category(""), inject_scope)] +pub fn runtime(_: impl Ctx, #[scope(editor_api::IDENTIFIER)] editor_api: Arc) -> RuntimeHandle { + editor_api.runtime.clone() +} diff --git a/node-graph/nodes/math/src/lib.rs b/node-graph/nodes/math/src/lib.rs index 8e7f9785c1..f485f44d7d 100644 --- a/node-graph/nodes/math/src/lib.rs +++ b/node-graph/nodes/math/src/lib.rs @@ -1,4 +1,5 @@ use core_types::Context; +use core_types::gpoll::GPoll; use core_types::list::List; use core_types::registry::types::{Fraction, Percentage, PixelSize}; use core_types::transform::Footprint; @@ -740,7 +741,7 @@ fn logical_not( /// Evaluates either the "If True" or "If False" input branch based on whether the input condition is true or false. #[node_macro::node(category("Math: Logic"))] -async fn switch( +fn switch( #[implementations(Context)] ctx: C, condition: bool, #[expose] @@ -781,8 +782,8 @@ async fn switch( Context -> List, )] if_false: impl Node, -) -> T { - if condition { if_true.eval(ctx).await } else { if_false.eval(ctx).await } +) -> GPoll { + if condition { if_true.eval(ctx) } else { if_false.eval(ctx) } } /// Constructs a bool value which may be set to true or false. @@ -988,74 +989,279 @@ fn normalize(_: impl Ctx, vector: DVec2) -> DVec2 { #[cfg(test)] mod test { use super::*; - use core_types::Node; - use core_types::generic::FnNode; #[test] pub fn dot_product_function() { let vector_a = DVec2::new(1., 2.); let vector_b = DVec2::new(3., 4.); - assert_eq!(dot_product((), vector_a, vector_b, false), 11.); + assert_eq!(dot_product(&(), vector_a, vector_b, false), 11.); } #[test] pub fn length_function() { let vector = DVec2::new(3., 4.); - assert_eq!(length((), vector), 5.); + assert_eq!(length(&(), vector), 5.); } #[test] fn test_basic_expression() { - let result = math((), 0., "2 + 2".to_string(), 0.); + let result = math(&(), 0., "2 + 2".to_string(), 0.); assert_eq!(result, 4.); } #[test] fn test_complex_expression() { - let result = math((), 0., "(5 * 3) + (10 / 2)".to_string(), 0.); + let result = math(&(), 0., "(5 * 3) + (10 / 2)".to_string(), 0.); assert_eq!(result, 20.); } #[test] fn test_default_expression() { - let result = math((), 0., "0".to_string(), 0.); + let result = math(&(), 0., "0".to_string(), 0.); assert_eq!(result, 0.); } #[test] fn test_invalid_expression() { - let result = math((), 0., "invalid".to_string(), 0.); + let result = math(&(), 0., "invalid".to_string(), 0.); assert_eq!(result, 0.); } - #[test] - pub fn foo() { - let fnn = FnNode::new(|(a, b)| (b, a)); - assert_eq!(fnn.eval((1u32, 2u32)), (2, 1)); - } - #[test] pub fn add_vectors() { - assert_eq!(super::add((), DVec2::ONE, DVec2::ONE), DVec2::ONE * 2.); + assert_eq!(super::add(&(), DVec2::ONE, DVec2::ONE), DVec2::ONE * 2.); } #[test] pub fn subtract_f64() { - assert_eq!(super::subtract((), 5_f64, 3_f64), 2.); + assert_eq!(super::subtract(&(), 5_f64, 3_f64), 2.); } #[test] pub fn divide_vectors() { - assert_eq!(super::divide((), DVec2::ONE, 2_f64), DVec2::ONE / 2.); + assert_eq!(super::divide(&(), DVec2::ONE, 2_f64), DVec2::ONE / 2.); } #[test] pub fn modulo_positive() { - assert_eq!(super::modulo((), -5_f64, 2_f64, true), 1_f64); + assert_eq!(super::modulo(&(), -5_f64, 2_f64, true), 1_f64); } #[test] pub fn modulo_negative() { - assert_eq!(super::modulo((), -5_f64, 2_f64, false), -1_f64); + assert_eq!(super::modulo(&(), -5_f64, 2_f64, false), -1_f64); + } +} + +#[cfg(test)] +mod graphene_test { + use super::*; + use core_types::arena::Arena; + use core_types::context::{ContextImpl, EvalScope, ExtractIndex}; + use core_types::gpoll::{Finality, GPoll}; + use core_types::node::{BatchStatus, Node}; + use core_types::registry::{EdgeHandle, ErasedNode, construct}; + use std::mem::MaybeUninit; + use std::sync::Arc; + + struct SourceNode(T); + + impl Node for SourceNode { + type Output = T; + + fn eval(&self, _input: &Input) -> GPoll { + GPoll::Final(self.0.clone()) + } + } + + struct IndexNode; + + impl Node for IndexNode { + type Output = f64; + + fn eval(&self, input: &Input) -> GPoll { + GPoll::Final(input.innermost_index() as f64) + } + } + + fn scope_fixture(arena: &Arena) -> EvalScope<'_> { + EvalScope::new(None, None, None, &[], arena) + } + + #[test] + fn generated_add_evaluates_through_the_node_path() { + let arena = Arena::new(64).unwrap(); + let scope = scope_fixture(&arena); + let ctx = ContextImpl::root(&scope); + + let graph = AddNode::new(SourceNode(1.0f64), SourceNode(2.0f64)); + assert_eq!(Node::eval(&graph, &ctx), GPoll::Final(3.0)); + } + + #[test] + fn generated_add_batches_through_the_erased_edge() { + let arena = Arena::new(64).unwrap(); + let scope = scope_fixture(&arena); + let ctx = ContextImpl::root(&scope); + + let erased: Box> = Box::new(AddNode::new(IndexNode, SourceNode(10.0f64))); + let mut scratch = [const { MaybeUninit::uninit() }; 4]; + let status = erased.eval_batch(&ctx, 2..6, Some(&mut scratch)); + let BatchStatus::Filled(lanes, finality) = status else { + panic!("expected filled, got {status:?}"); + }; + assert_eq!(lanes.values(), &[12.0, 13.0, 14.0, 15.0]); + assert_eq!(finality, Finality::AllFinal); + } + + #[test] + fn generated_wire_constructor_resolves_and_wires() { + let arena = Arena::new(64).unwrap(); + let scope = scope_fixture(&arena); + let ctx = ContextImpl::root(&scope); + + let entries = logical_or_entries(); + let value = EdgeHandle::new(Arc::new(SourceNode(true)) as Arc>); + let other_value = EdgeHandle::new(Arc::new(SourceNode(false)) as Arc>); + let wired = construct(&entries[0], vec![value, other_value]).unwrap().downcast::().unwrap(); + + assert_eq!(Node::eval(&wired, &ctx), GPoll::Final(true)); + } + + #[test] + fn ctor_registration_populates_the_node_registry() { + let registry = core_types::registry::NODE_REGISTRY.lock().unwrap(); + let rows = registry + .iter() + .find_map(|(id, rows)| id.as_str().ends_with("::AddNode").then_some(rows)) + .expect("AddNode rows registered at startup"); + assert_eq!(rows.len(), 6); + } + + #[test] + fn generic_add_registers_one_entry_per_implementation() { + let arena = Arena::new(64).unwrap(); + let scope = scope_fixture(&arena); + let ctx = ContextImpl::root(&scope); + + let entries = add_entries(); + assert_eq!(entries.len(), 6); + assert_eq!(entries[0].io.inputs, vec![core_types::concrete!(f64), core_types::concrete!(f64)]); + assert_eq!(entries[0].io.return_value, core_types::concrete!(f64)); + assert_eq!(entries[3].io.inputs, vec![core_types::concrete!(DVec2), core_types::concrete!(DVec2)]); + assert_eq!(entries[3].io.return_value, core_types::concrete!(DVec2)); + + let augend = EdgeHandle::new(Arc::new(SourceNode(1.5f64)) as Arc>); + let addend = EdgeHandle::new(Arc::new(SourceNode(2.5f64)) as Arc>); + let wired = construct(&entries[0], vec![augend, addend]).unwrap().downcast::().unwrap(); + + assert_eq!(Node::eval(&wired, &ctx), GPoll::Final(4.0)); + } + + #[test] + fn converted_switch_evaluates_only_the_taken_branch() { + use std::sync::Arc; + use std::sync::atomic::{AtomicU32, Ordering}; + + struct CountingSource(Arc, f64); + + impl Node for CountingSource { + type Output = f64; + + fn eval(&self, _input: &Input) -> GPoll { + self.0.fetch_add(1, Ordering::Relaxed); + GPoll::Final(self.1) + } + } + + let arena = Arena::new(64).unwrap(); + let scope = scope_fixture(&arena); + let ctx = ContextImpl::root(&scope); + + let taken = Arc::new(AtomicU32::new(0)); + let untaken = Arc::new(AtomicU32::new(0)); + let graph = SwitchNode::new(SourceNode(true), CountingSource(taken.clone(), 1.0), CountingSource(untaken.clone(), 2.0)); + + assert_eq!(Node::eval(&graph, &ctx), GPoll::Final(1.0)); + assert_eq!(taken.load(Ordering::Relaxed), 1); + assert_eq!(untaken.load(Ordering::Relaxed), 0); + } + + #[test] + fn converted_switch_passes_branch_status_through() { + struct PendingSource; + + impl Node for PendingSource { + type Output = f64; + + fn eval(&self, _input: &Input) -> GPoll { + GPoll::Pending + } + } + + struct PartialSource; + + impl Node for PartialSource { + type Output = f64; + + fn eval(&self, _input: &Input) -> GPoll { + GPoll::Partial(7.0) + } + } + + let arena = Arena::new(64).unwrap(); + let scope = scope_fixture(&arena); + let ctx = ContextImpl::root(&scope); + + let pending = SwitchNode::new(SourceNode(true), PendingSource, PartialSource); + assert_eq!(Node::eval(&pending, &ctx), GPoll::Pending); + + let partial = SwitchNode::new(SourceNode(false), PendingSource, PartialSource); + assert_eq!(Node::eval(&partial, &ctx), GPoll::Partial(7.0)); + } + + #[test] + fn converted_switch_merges_condition_status_into_the_branch_result() { + struct PartialCondition; + + impl Node for PartialCondition { + type Output = bool; + + fn eval(&self, _input: &Input) -> GPoll { + GPoll::Partial(true) + } + } + + let arena = Arena::new(64).unwrap(); + let scope = scope_fixture(&arena); + let ctx = ContextImpl::root(&scope); + + let graph = SwitchNode::new(PartialCondition, SourceNode(1.0f64), SourceNode(2.0f64)); + assert_eq!(Node::eval(&graph, &ctx), GPoll::Partial(1.0)); + } + + #[test] + fn generated_eval_computes_on_stand_in_and_traces_fallback() { + struct FallbackNode; + + impl Node for FallbackNode { + type Output = f64; + + fn eval(&self, _input: &Input) -> GPoll { + GPoll::fallback(0.0, "upstream failed") + } + } + + let arena = Arena::new(64).unwrap(); + let scope = scope_fixture(&arena); + let ctx = ContextImpl::root(&scope); + + let graph = AddNode::new(FallbackNode, SourceNode(5.0f64)); + let GPoll::Fallback(boxed) = Node::eval(&graph, &ctx) else { + panic!("fallback must propagate with the computed stand-in"); + }; + assert_eq!(boxed.0, 5.0); + assert!(boxed.1.kind == "upstream failed"); + assert_eq!(boxed.1.trace, vec![0]); } } diff --git a/node-graph/nodes/path-bool/src/lib.rs b/node-graph/nodes/path-bool/src/lib.rs index 14e2270f98..cc4dc9fbd1 100644 --- a/node-graph/nodes/path-bool/src/lib.rs +++ b/node-graph/nodes/path-bool/src/lib.rs @@ -23,7 +23,7 @@ pub use vector_types::vector::misc::BooleanOperation; /// Combines the geometric forms of one or more closed paths into a new vector path that results from cutting or joining the paths by the chosen method. #[node_macro::node(category("Vector: Modifier"), memoize)] -async fn boolean_operation( +fn boolean_operation( _: impl Ctx, /// The `List` of vector paths to perform the boolean operation on. Nested `List`s are automatically flattened. #[implementations(List, List)] diff --git a/node-graph/nodes/raster/src/blending_nodes.rs b/node-graph/nodes/raster/src/blending_nodes.rs index 1b6c4f6977..ab7378b636 100644 --- a/node-graph/nodes/raster/src/blending_nodes.rs +++ b/node-graph/nodes/raster/src/blending_nodes.rs @@ -201,8 +201,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); @@ -212,7 +212,7 @@ mod test { // 100% of the output should come from the multiplied value let opacity = 100.; - let result = super::color_overlay((), List::new_from_element(Raster::new_cpu(image.clone())), overlay_color, BlendMode::Multiply, opacity); + let result = super::color_overlay(&(), List::new_from_element(Raster::new_cpu(image.clone())), overlay_color, BlendMode::Multiply, opacity); let result = result.element(0).unwrap().clone(); // The output should just be the original green and alpha channels (as we multiply them by 1 and other channels by 0) diff --git a/node-graph/nodes/raster/src/dehaze.rs b/node-graph/nodes/raster/src/dehaze.rs index b236dafa52..711fb47b0f 100644 --- a/node-graph/nodes/raster/src/dehaze.rs +++ b/node-graph/nodes/raster/src/dehaze.rs @@ -8,7 +8,7 @@ use raster_types::{CPU, Raster}; use std::cmp::{max, min}; #[node_macro::node(category("Raster: Filter"))] -async fn dehaze(_: impl Ctx, image_frame: List>, strength: Percentage) -> List> { +fn dehaze(_: impl Ctx, image_frame: List>, strength: Percentage) -> List> { image_frame .into_iter() .map(|mut row| { diff --git a/node-graph/nodes/raster/src/filter.rs b/node-graph/nodes/raster/src/filter.rs index 373298a016..cfa29435d0 100644 --- a/node-graph/nodes/raster/src/filter.rs +++ b/node-graph/nodes/raster/src/filter.rs @@ -87,7 +87,7 @@ fn unpremultiply_gamma_to_linear(buffer: Image) -> Imag /// Blurs the image with a Gaussian or box blur kernel filter. #[node_macro::node(category("Raster: Filter"))] -async fn blur( +fn blur( _: impl Ctx, /// The image to be blurred. image_frame: List>, @@ -124,7 +124,7 @@ async fn blur( /// Applies a median filter to reduce noise while preserving edges. #[node_macro::node(category("Raster: Filter"))] -async fn median_filter( +fn median_filter( _: impl Ctx, /// The image to be filtered. image_frame: List>, diff --git a/node-graph/nodes/raster/src/gradient_map.rs b/node-graph/nodes/raster/src/gradient_map.rs index db3b949447..c314332a8c 100644 --- a/node-graph/nodes/raster/src/gradient_map.rs +++ b/node-graph/nodes/raster/src/gradient_map.rs @@ -10,7 +10,7 @@ use vector_types::GradientStops; // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27grdm%27%20%3D%20Gradient%20Map // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=Gradient%20settings%20(Photoshop%206.0) #[node_macro::node(category("Raster: Adjustment"))] -async fn gradient_map>( +fn gradient_map>( _: impl Ctx, #[implementations( List>, diff --git a/node-graph/nodes/raster/src/image_color_palette.rs b/node-graph/nodes/raster/src/image_color_palette.rs index 240e51d5ff..b91e487f18 100644 --- a/node-graph/nodes/raster/src/image_color_palette.rs +++ b/node-graph/nodes/raster/src/image_color_palette.rs @@ -4,7 +4,7 @@ use core_types::list::{Item, List}; use raster_types::{CPU, Raster}; #[node_macro::node(category("Color"))] -async fn image_color_palette( +fn image_color_palette( _: impl Ctx, image: List>, #[default(4)] @@ -68,7 +68,7 @@ mod test { #[test] fn test_image_color_palette() { let result = image_color_palette( - (), + &(), List::new_from_element(Raster::new_cpu(Image { width: 100, height: 100, @@ -77,6 +77,6 @@ mod test { })), 1, ); - assert_eq!(futures::executor::block_on(result), List::new_from_element(Color::from_rgbaf32(0., 0., 0., 1.).unwrap())); + assert_eq!(result, List::new_from_element(Color::from_rgbaf32(0., 0., 0., 1.).unwrap())); } } diff --git a/node-graph/nodes/raster/src/std_nodes.rs b/node-graph/nodes/raster/src/std_nodes.rs index 6ff551843d..3556b2ef26 100644 --- a/node-graph/nodes/raster/src/std_nodes.rs +++ b/node-graph/nodes/raster/src/std_nodes.rs @@ -241,7 +241,7 @@ pub fn extend_image_to_bounds(_: impl Ctx, image: List>, bounds: DAf let image_data = &row.element().data; let (image_width, image_height) = (row.element().width, row.element().height); if image_width == 0 || image_height == 0 { - return empty_image((), bounds, List::new_from_element(Color::TRANSPARENT)).into_iter().next().unwrap(); + return empty_image(&(), bounds, List::new_from_element(Color::TRANSPARENT)).into_iter().next().unwrap(); } let orig_image_scale = DVec2::new(image_width as f64, image_height as f64); @@ -290,7 +290,7 @@ pub fn empty_image(_: impl Ctx, transform: DAffine2, color: List) -> List } #[node_macro::node(category(""))] -pub fn image<'a: 'n>(_: impl Ctx, resource: Resource) -> List> { +pub fn image(_: impl Ctx, resource: Resource) -> List> { let image_data = resource.as_ref(); let Some(image) = ::image::load_from_memory(image_data).ok() else { diff --git a/node-graph/nodes/repeat/Cargo.toml b/node-graph/nodes/repeat/Cargo.toml index ec3c38e495..9f2d9e261c 100644 --- a/node-graph/nodes/repeat/Cargo.toml +++ b/node-graph/nodes/repeat/Cargo.toml @@ -24,9 +24,3 @@ log = { workspace = true } # Optional workspace dependencies serde = { workspace = true, optional = true } - -[dev-dependencies] -graphene-core = { workspace = true } -vector-nodes = { workspace = true } -tokio = { workspace = true, features = ["macros", "rt"] } -kurbo = { workspace = true } diff --git a/node-graph/nodes/repeat/src/repeat_nodes.rs b/node-graph/nodes/repeat/src/repeat_nodes.rs index a4137dc978..a277dc53d7 100644 --- a/node-graph/nodes/repeat/src/repeat_nodes.rs +++ b/node-graph/nodes/repeat/src/repeat_nodes.rs @@ -1,16 +1,17 @@ use crate::gcore::Context; use core::f64::consts::TAU; +use core_types::gpoll::Interrupt; use core_types::list::List; use core_types::registry::types::{Angle, PixelSize}; -use core_types::{ATTR_TRANSFORM, CloneVarArgs, Color, Ctx, ExtractAll, InjectVarArgs, OwnedContextImpl}; +use core_types::{ATTR_TRANSFORM, Color, Ctx, DeriveCtx, InjectVarArgs}; use glam::{DAffine2, DVec2}; use graphic_types::{Graphic, Vector}; use raster_types::{CPU, Raster}; use vector_types::GradientStops; #[node_macro::node(category("Repeat"))] -async fn repeat + Default + Send + Clone + 'static>( - ctx: impl ExtractAll + CloneVarArgs + Ctx, +fn repeat + Default + Send + Clone + 'static>( + ctx: impl Ctx + DeriveCtx, #[implementations( Context -> List, Context -> List, @@ -18,35 +19,35 @@ async fn repeat + Default + Send + Clone + 'static>( Context -> List, Context -> List, )] - content: impl Node<'n, Context<'static>, Output = List>, + content: impl Node, Output = List>, #[default(1)] #[hard(1..)] count: u32, reverse: bool, -) -> List { +) -> Result, Interrupt> { // Someday this node can have the option to generate infinitely instead of a fixed count (basically `std::iter::repeat`). - let count = count as usize; + let count = count as u64; + let spilled = ctx.index_head(); let mut result_list = List::new(); for index in 0..count { 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(&ctx.promoted(&spilled, index))?; for generated_row in generated_content.into_iter() { result_list.push(generated_row); } } - result_list + Ok(result_list) } #[node_macro::node(category("Repeat"))] -pub async fn repeat_array + Default + Send + Clone + 'static>( - ctx: impl ExtractAll + CloneVarArgs + Ctx, +pub fn repeat_array + Default + Send + Clone + 'static>( + ctx: impl Ctx + DeriveCtx, #[implementations( Context -> List, Context -> List, @@ -54,7 +55,7 @@ pub async fn repeat_array + Default + Send + Clone + 'static>( Context -> List, Context -> List, )] - content: impl Node<'n, Context<'static>, Output = List>, + content: impl Node, Output = List>, #[default(100., 100.)] // TODO: When using a custom Properties panel layout in document_node_definitions.rs and this default is set, the widget weirdly doesn't show up in the Properties panel. Investigation is needed. direction: PixelSize, @@ -62,10 +63,11 @@ pub async fn repeat_array + Default + Send + Clone + 'static>( #[default(5)] #[hard(1..)] count: u32, -) -> List { +) -> Result, Interrupt> { 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 let total = (count - 1).max(1) as f64; + let spilled = ctx.index_head(); let mut result_list = List::new(); @@ -74,8 +76,7 @@ pub async fn repeat_array + Default + Send + Clone + 'static>( let translation = index as f64 * direction / total; 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(&ctx.promoted(&spilled, index as u64))?; for row_index in 0..generated_content.len() { let Some(mut row) = generated_content.clone_item(row_index) else { continue }; @@ -89,12 +90,12 @@ pub async fn repeat_array + Default + Send + Clone + 'static>( } } - result_list + Ok(result_list) } #[node_macro::node(category("Repeat"))] -async fn repeat_radial + Default + Send + Clone + 'static>( - ctx: impl ExtractAll + CloneVarArgs + Ctx, +fn repeat_radial + Default + Send + Clone + 'static>( + ctx: impl Ctx + DeriveCtx, #[implementations( Context -> List, Context -> List, @@ -102,7 +103,7 @@ async fn repeat_radial + Default + Send + Clone + 'static>( Context -> List, Context -> List, )] - content: impl Node<'n, Context<'static>, Output = List>, + content: impl Node, Output = List>, start_angle: Angle, #[unit(" px")] #[default(5)] @@ -110,7 +111,8 @@ async fn repeat_radial + Default + Send + Clone + 'static>( #[default(5)] #[hard(1..)] count: u32, -) -> List { +) -> Result, Interrupt> { + let spilled = ctx.index_head(); let mut result_list = List::new(); for index in 0..count { @@ -118,8 +120,7 @@ async fn repeat_radial + Default + Send + Clone + 'static>( let translation = DAffine2::from_translation(radius * DVec2::Y); 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(&ctx.promoted(&spilled, index as u64))?; for row_index in 0..generated_content.len() { let Some(mut row) = generated_content.clone_item(row_index) else { continue }; @@ -133,12 +134,12 @@ async fn repeat_radial + Default + Send + Clone + 'static>( } } - result_list + Ok(result_list) } #[node_macro::node(category("Repeat"), name("Repeat on Points"))] -async fn repeat_on_points + Default + Send + Clone + 'static>( - ctx: impl ExtractAll + CloneVarArgs + Sync + Ctx + InjectVarArgs, +fn repeat_on_points + Default + Send + Clone + 'static>( + ctx: impl Ctx + DeriveCtx + InjectVarArgs, points: List, #[implementations( Context -> List, @@ -147,178 +148,176 @@ async fn repeat_on_points + Default + Send + Clone + 'static>( Context -> List, Context -> List, )] - content: impl Node<'n, Context<'static>, Output = List>, + content: impl Node, Output = List>, reverse: bool, -) -> List { +) -> Result, Interrupt> { + let spilled = ctx.index_head(); let mut result_list = List::new(); for points_index in 0..points.len() { 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 positions = points_element.point_domain.positions(); + let range: Box> = match reverse { + true => Box::new(positions.iter().enumerate().rev()), + false => Box::new(positions.iter().enumerate()), + }; + + for (index, &point) in range { 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 scoped = ctx.push_position(transformed_point); + let generated_content = content.eval(&scoped.ctx().promoted(&spilled, index as u64))?; 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); } - }; - - let range = points_element.point_domain.positions().iter().enumerate(); - if reverse { - for (index, &point) in range.rev() { - iteration(index, point).await; - } - } else { - for (index, &point) in range { - iteration(index, point).await; - } } } - result_list + Ok(result_list) } #[cfg(test)] mod test { use super::*; - use core_types::Ctx; - use core_types::Node; - use core_types::transform::Footprint; - use glam::DVec2; - use graphene_core::ReadPositionNode; - use graphene_core::extract_xy::{ExtractXyNode, XY}; - use graphic_types::Vector; - use kurbo::Shape; - use kurbo::{BezPath, DEFAULT_ACCURACY, Rect}; - use std::future::Future; - use std::pin::Pin; - use vector_nodes::generator_nodes::RectangleNode; + use core_types::arena::Arena; + use core_types::context::{ContextImpl, EvalScope, ExtractIndex, ExtractPosition}; + use core_types::gpoll::GPoll; + use core_types::list::Item; + use core_types::node::{LazyInput, Node, StatusCell}; use vector_types::subpath::Subpath; - fn vector_node_from_bezpath(bezpath: BezPath) -> List { - List::new_from_element(Vector::from_bezpath(bezpath)) - } + const TEST_POSITION: &str = "test-position"; - #[derive(Clone)] - pub struct FutureWrapperNode(T); + 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: &Input) -> GPoll { + GPoll::Final(self.0.clone()) } } - #[tokio::test] - async fn repeat_on_points_test() { - let context = OwnedContextImpl::default().into_context(); - let rect = RectangleNode::new( - FutureWrapperNode(()), - ExtractXyNode::new(ReadPositionNode::new(FutureWrapperNode(()), FutureWrapperNode(0)), FutureWrapperNode(XY::Y)), - FutureWrapperNode(2_f64), - FutureWrapperNode(false), - FutureWrapperNode(0_f64), - FutureWrapperNode(false), - ); + /// Returns one default `Vector` whose transform records the innermost context index in its x translation. + struct IndexProbe; - let positions = [DVec2::new(40., 20.), DVec2::ONE, DVec2::new(-42., 9.), DVec2::new(10., 345.)]; - let points = List::new_from_element(Vector::from_subpath(Subpath::from_anchors(positions, false))); - let generated = super::repeat_on_points(context, points, &rect, false).await; - assert_eq!(generated.len(), positions.len()); - for (position, index) in positions.into_iter().zip(0..generated.len()) { - let bounds = generated - .element(index) - .unwrap() - .bounding_box_with_transform(generated.attribute_cloned_or_default(ATTR_TRANSFORM, index)) - .unwrap(); - assert!(position.abs_diff_eq((bounds[0] + bounds[1]) / 2., 1e-10)); - assert_eq!((bounds[1] - bounds[0]).x, position.y); + impl Node for IndexProbe { + type Output = List; + + fn eval(&self, input: &Input) -> GPoll> { + let index = input.try_index().and_then(|mut levels| levels.next()).expect("repeat must push an index level"); + let mut list = List::new(); + list.push(Item::new_from_element(Vector::default()).with_attribute(ATTR_TRANSFORM, DAffine2::from_translation(DVec2::new(index as f64, 0.)))); + GPoll::Final(list) } } - #[tokio::test] - async fn repeat() { - let direction = DVec2::X * 1.5; - let count = 3; - let context = OwnedContextImpl::default().into_context(); - let repeated = super::repeat_array( - context, - &FutureWrapperNode(vector_node_from_bezpath(Rect::new(0., 0., 1., 1.).to_path(DEFAULT_ACCURACY))), - direction, - 0., - count, - ) - .await; - let vector_list = vector_nodes::flatten_path(Footprint::default(), repeated).await; - let vector = vector_list.element(0).unwrap(); - assert_eq!(vector.region_manipulator_groups().count(), 3); - for (index, (_, manipulator_groups)) in vector.region_manipulator_groups().enumerate() { - assert!((manipulator_groups[0].anchor - direction * index as f64 / (count - 1) as f64).length() < 1e-5); + /// Returns one default `Vector` recording the innermost context position under `TEST_POSITION`. + struct PositionProbe; + + impl Node for PositionProbe { + type Output = List; + + fn eval(&self, input: &Input) -> GPoll> { + let position = input.try_position().and_then(|mut positions| positions.next()).expect("repeat_on_points must push a position level"); + let mut list = List::new(); + list.push(Item::new_from_element(Vector::default()).with_attribute(TEST_POSITION, DAffine2::from_translation(position))); + GPoll::Final(list) } } - #[tokio::test] - async fn repeat_single_copy() { - let context = OwnedContextImpl::default().into_context(); - let repeated = super::repeat_array( - context, - &FutureWrapperNode(vector_node_from_bezpath(Rect::new(0., 0., 1., 1.).to_path(DEFAULT_ACCURACY))), - DVec2::new(12., 10.), - 45., - 1, - ) - .await; - let vector_list = vector_nodes::flatten_path(Footprint::default(), repeated).await; - let vector = vector_list.element(0).unwrap(); - assert_eq!(vector.region_manipulator_groups().count(), 1); - - let (_, manipulator_groups) = vector.region_manipulator_groups().next().unwrap(); - let anchor = manipulator_groups[0].anchor; - assert!(anchor.length() < 1e-5, "Expected the single copy to be untransformed, found anchor {anchor}"); + fn single_default_vector() -> List { + List::new_from_element(Vector::default()) + } + + fn row_translations(list: &List, key: &str) -> Vec { + (0..list.len()).map(|index| list.attribute_cloned_or_default::(key, index).translation).collect() + } + + macro_rules! test_ctx { + ($ctx:ident, $cell:ident) => { + let arena = Arena::new(4096).unwrap(); + let generations = []; + let scope = EvalScope::new(None, None, None, &generations, &arena); + let $ctx = ContextImpl::root(&scope); + let $cell = StatusCell::default(); + }; } - #[tokio::test] - async fn repeat_transform_position() { - let direction = DVec2::new(12., 10.); - let count = 8; - let context = OwnedContextImpl::default().into_context(); - let repeated = super::repeat_array( - context, - &FutureWrapperNode(vector_node_from_bezpath(Rect::new(0., 0., 1., 1.).to_path(DEFAULT_ACCURACY))), - direction, - 0., - count, - ) - .await; - let vector_list = vector_nodes::flatten_path(Footprint::default(), repeated).await; - let vector = vector_list.element(0).unwrap(); - assert_eq!(vector.region_manipulator_groups().count(), 8); - for (index, (_, manipulator_groups)) in vector.region_manipulator_groups().enumerate() { - assert!((manipulator_groups[0].anchor - direction * index as f64 / (count - 1) as f64).length() < 1e-5); + #[test] + fn repeat_pushes_the_iteration_index_in_order() { + test_ctx!(ctx, cell); + + let x_translations = |values: [f64; 3]| values.map(|x| DVec2::new(x, 0.)).to_vec(); + + let forward = super::repeat(&ctx, LazyInput::new(&IndexProbe, &cell, 0), 3, false).unwrap(); + assert_eq!(row_translations(&forward, ATTR_TRANSFORM), x_translations([0., 1., 2.])); + + let reversed = super::repeat(&ctx, LazyInput::new(&IndexProbe, &cell, 0), 3, true).unwrap(); + assert_eq!(row_translations(&reversed, ATTR_TRANSFORM), x_translations([2., 1., 0.])); + } + + #[test] + fn repeat_array_spaces_copies_along_the_direction() { + test_ctx!(ctx, cell); + let direction = DVec2::new(1.5, 0.); + let count = 3; + + let content = ValueNode(single_default_vector()); + let repeated = super::repeat_array(&ctx, LazyInput::new(&content, &cell, 0), direction, 0., count).unwrap(); + + assert_eq!(repeated.len(), count as usize); + for (index, translation) in row_translations(&repeated, ATTR_TRANSFORM).into_iter().enumerate() { + let expected = index as f64 * direction / (count - 1) as f64; + assert!(translation.abs_diff_eq(expected, 1e-10), "copy {index}: {translation:?} != {expected:?}"); } } - #[tokio::test] - async fn repeat_radial() { - let context = OwnedContextImpl::default().into_context(); - let repeated = super::repeat_radial(context, &FutureWrapperNode(vector_node_from_bezpath(Rect::new(-1., -1., 1., 1.).to_path(DEFAULT_ACCURACY))), 45., 4., 8).await; - let vector_list = vector_nodes::flatten_path(Footprint::default(), repeated).await; - let vector = vector_list.element(0).unwrap(); - assert_eq!(vector.region_manipulator_groups().count(), 8); + #[test] + fn repeat_array_single_copy_stays_finite() { + test_ctx!(ctx, cell); - for (index, (_, manipulator_groups)) in vector.region_manipulator_groups().enumerate() { - let expected_angle = (index as f64 + 1.) * 45.; + let content = ValueNode(single_default_vector()); + let repeated = super::repeat_array(&ctx, LazyInput::new(&content, &cell, 0), DVec2::new(12., 10.), 45., 1).unwrap(); - let center = (manipulator_groups[0].anchor + manipulator_groups[2].anchor) / 2.; - let actual_angle = DVec2::Y.angle_to(center).to_degrees(); + assert_eq!(repeated.len(), 1); + let transform: DAffine2 = repeated.attribute_cloned_or_default(ATTR_TRANSFORM, 0); + assert!(transform.abs_diff_eq(DAffine2::IDENTITY, 1e-10), "single copy must not divide by zero: {transform:?}"); + } + + #[test] + fn repeat_radial_rotates_copies_around_the_center() { + test_ctx!(ctx, cell); + let (radius, count) = (5., 4); + + let content = ValueNode(single_default_vector()); + let repeated = super::repeat_radial(&ctx, LazyInput::new(&content, &cell, 0), 0., radius, count).unwrap(); - assert!((actual_angle - expected_angle).abs() % 360. < 1e-5, "Expected {expected_angle} found {actual_angle}"); + assert_eq!(repeated.len(), count as usize); + for index in 0..count as usize { + let transform: DAffine2 = repeated.attribute_cloned_or_default(ATTR_TRANSFORM, index); + let expected = DAffine2::from_angle((TAU / count as f64) * index as f64) * DAffine2::from_translation(radius * DVec2::Y); + assert!(transform.abs_diff_eq(expected, 1e-10), "copy {index}: {transform:?} != {expected:?}"); } } + + #[test] + fn repeat_on_points_pushes_each_point_as_the_position() { + test_ctx!(ctx, cell); + let positions = [DVec2::new(40., 20.), DVec2::ONE, DVec2::new(-42., 9.), DVec2::new(10., 345.)]; + let points = List::new_from_element(Vector::from_subpath(Subpath::from_anchors(positions, false))); + + let generated = super::repeat_on_points(&ctx, points.clone(), LazyInput::new(&PositionProbe, &cell, 0), false).unwrap(); + assert_eq!(row_translations(&generated, ATTR_TRANSFORM), positions.to_vec()); + assert_eq!(row_translations(&generated, TEST_POSITION), positions.to_vec()); + + let reversed = super::repeat_on_points(&ctx, points, LazyInput::new(&PositionProbe, &cell, 0), true).unwrap(); + let mut expected = positions.to_vec(); + expected.reverse(); + assert_eq!(row_translations(&reversed, ATTR_TRANSFORM), expected); + } } diff --git a/node-graph/nodes/text/src/lib.rs b/node-graph/nodes/text/src/lib.rs index 489ce33d54..5a8ae12eb2 100644 --- a/node-graph/nodes/text/src/lib.rs +++ b/node-graph/nodes/text/src/lib.rs @@ -7,10 +7,11 @@ mod text_context; mod to_path; use convert_case::{Boundary, Converter, pattern}; +use core_types::gpoll::Interrupt; use core_types::graphene_hash::CacheHash; use core_types::list::{Item, List}; use core_types::registry::types::{SignedInteger, TextArea}; -use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, ExtractVarArgs, OwnedContextImpl}; +use core_types::{Context, Ctx, DeriveCtx, ExtractVarArgs}; use dyn_any::DynAny; use glam::{DAffine2, DVec2}; use unicode_segmentation::UnicodeSegmentation; @@ -768,25 +769,25 @@ fn string_join( /// Iterates over a list of strings, evaluating the mapped operation for each one. Use the **Read String** node to access the current string inside the loop. #[node_macro::node(category("Text"))] -async fn map_string( - ctx: impl Ctx + CloneVarArgs + ExtractAll, +fn map_string( + ctx: impl Ctx + DeriveCtx, strings: List, #[expose] #[implementations(Context -> String)] - mapped: impl Node, Output = String>, -) -> List { + mapped: impl Node, Output = String>, +) -> Result, Interrupt> { + let spilled = ctx.index_head(); let mut result = List::new(); for (i, row) in strings.into_iter().enumerate() { let string = row.into_element(); - let owned_ctx = OwnedContextImpl::from(ctx.clone()); - let owned_ctx = owned_ctx.with_vararg(Box::new(string)).with_index(i); - let mapped_string = mapped.eval(owned_ctx.into_context()).await; + let scoped = ctx.push_vararg(&string); + let mapped_string = mapped.eval(&scoped.ctx().promoted(&spilled, i as u64))?; result.push(Item::new_from_element(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 8ed04f2cfe..40930110fd 100644 --- a/node-graph/nodes/transform/src/transform_nodes.rs +++ b/node-graph/nodes/transform/src/transform_nodes.rs @@ -1,8 +1,9 @@ use core::f64; use core_types::color::Color; +use core_types::gpoll::Interrupt; use core_types::list::{List, ListDyn}; use core_types::transform::{ApplyTransform, ScaleType, Transform}; -use core_types::{ATTR_TRANSFORM, CloneVarArgs, Context, Ctx, ExtractAll, InjectFootprint, ModifyFootprint, OwnedContextImpl}; +use core_types::{ATTR_TRANSFORM, Context, Ctx, DeriveCtx, InjectFootprint, ModifyFootprint}; use glam::{DAffine2, DMat2, DVec2}; use graphic_types::Graphic; use graphic_types::Vector; @@ -11,8 +12,8 @@ use vector_types::GradientStops; /// Applies the specified transform to the input value, which may be a graphic type or another transform. #[node_macro::node(category("Math: Transform"))] -async fn transform( - ctx: impl Ctx + CloneVarArgs + ExtractAll + ModifyFootprint, +fn transform( + ctx: impl Ctx + DeriveCtx + ModifyFootprint, #[implementations( Context -> DAffine2, Context -> DVec2, @@ -24,31 +25,24 @@ async fn transform( Context -> List, Context -> List, )] - content: impl Node, Output = T>, + content: impl Node, Output = T>, #[widget(ParsedWidgetOverride::Custom = "transform_translation")] translation: DVec2, #[widget(ParsedWidgetOverride::Custom = "transform_rotation")] rotation: f64, #[widget(ParsedWidgetOverride::Custom = "transform_scale")] #[default(1., 1.)] scale: DVec2, #[widget(ParsedWidgetOverride::Custom = "transform_skew")] skew: DVec2, -) -> T { +) -> Result { let trs = DAffine2::from_scale_angle_translation(scale, rotation.to_radians(), translation); let skew = DAffine2::from_cols_array(&[1., skew.y.to_radians().tan(), skew.x.to_radians().tan(), 1., 0., 0.]); let matrix = trs * skew; - let footprint = ctx.try_footprint().copied(); - - let mut ctx = OwnedContextImpl::from(ctx); - if let Some(mut footprint) = footprint { - footprint.apply_transform(&matrix); - ctx = ctx.with_footprint(footprint); - } - - let mut transform_target = content.eval(ctx.into_context()).await; + let transformed = ctx.modify_footprint(|footprint| footprint.apply_transform(&matrix)); + let mut transform_target = content.eval(&transformed.ctx())?; transform_target.left_apply_transform(&matrix); - transform_target + Ok(transform_target) } /// 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. @@ -114,7 +108,7 @@ fn replace_transform( // TODO: Figure out how this node should behave once #2982 is implemented. /// Obtains the transform of the first item in the input `List`, if present. #[node_macro::node(category("Math: Transform"), path(core_types::vector))] -async fn extract_transform(_: impl Ctx, content: ListDyn) -> DAffine2 { +fn extract_transform(_: impl Ctx, content: ListDyn) -> DAffine2 { content.attribute::(ATTR_TRANSFORM, 0).copied().unwrap_or_default() } diff --git a/node-graph/nodes/vector/src/generator_nodes.rs b/node-graph/nodes/vector/src/generator_nodes.rs index 30c4e8c3d7..8f5a67044c 100644 --- a/node-graph/nodes/vector/src/generator_nodes.rs +++ b/node-graph/nodes/vector/src/generator_nodes.rs @@ -411,11 +411,11 @@ mod tests { #[test] fn isometric_grid_test() { // Doesn't crash with weird angles - grid((), (), GridType::Isometric, 0., 5, 5, (0., 0.).into()); - grid((), (), GridType::Isometric, 90., 5, 5, (90., 90.).into()); + grid(&(), (), GridType::Isometric, 0., 5, 5, (0., 0.).into()); + grid(&(), (), GridType::Isometric, 90., 5, 5, (90., 90.).into()); // Works properly - let grid = grid((), (), GridType::Isometric, 10., 5, 5, (30., 30.).into()); + let grid = grid(&(), (), GridType::Isometric, 10., 5, 5, (30., 30.).into()); assert_eq!(grid.element(0).unwrap().point_domain.ids().len(), 5 * 5); assert_eq!(grid.element(0).unwrap().segment_bezier_iter().count(), 4 * 5 + 4 * 9); for (_, bezier, _, _) in grid.element(0).unwrap().segment_bezier_iter() { @@ -430,7 +430,7 @@ mod tests { #[test] fn skew_isometric_grid_test() { - let grid = grid((), (), GridType::Isometric, 10., 5, 5, (40., 30.).into()); + let grid = grid(&(), (), GridType::Isometric, 10., 5, 5, (40., 30.).into()); assert_eq!(grid.element(0).unwrap().point_domain.ids().len(), 5 * 5); assert_eq!(grid.element(0).unwrap().segment_bezier_iter().count(), 4 * 5 + 4 * 9); for (_, bezier, _, _) in grid.element(0).unwrap().segment_bezier_iter() { @@ -443,7 +443,7 @@ mod tests { #[test] fn qr_code_test() { - let qr = qr_code((), (), "https://graphite.art".to_string(), false, 1., QRCodeErrorCorrectionLevel::Low, true); + let qr = qr_code(&(), (), "https://graphite.art".to_string(), false, 1., QRCodeErrorCorrectionLevel::Low, true); assert!(qr.element(0).unwrap().point_domain.ids().len() > 0); assert!(qr.element(0).unwrap().segment_domain.ids().len() > 0); } diff --git a/node-graph/nodes/vector/src/vector_modification_nodes.rs b/node-graph/nodes/vector/src/vector_modification_nodes.rs index e473c8184f..5123466c4e 100644 --- a/node-graph/nodes/vector/src/vector_modification_nodes.rs +++ b/node-graph/nodes/vector/src/vector_modification_nodes.rs @@ -7,7 +7,7 @@ use vector_types::vector::VectorModification; /// Applies a differential modification to a vector path, associating changes made by the Pen and Path tools to indices of edited points and segments. #[node_macro::node(category(""))] -async fn path_modify(_ctx: impl Ctx, mut vector: List, modification: Box, node_path: List) -> List { +fn path_modify(_ctx: impl Ctx, mut vector: List, modification: Box, node_path: List) -> List { use core_types::list::Item; if vector.is_empty() { @@ -35,7 +35,7 @@ async fn path_modify(_ctx: impl Ctx, mut vector: List, modification: Box /// Applies the vector path's local transformation to its geometry and resets the transform to the identity. #[node_macro::node(category("Vector"))] -async fn apply_transform(_ctx: impl Ctx, mut vector: List) -> List { +fn apply_transform(_ctx: impl Ctx, mut vector: List) -> List { let (elements, transforms) = vector.element_and_attribute_slices_mut::(ATTR_TRANSFORM); for (element, transform) in elements.iter_mut().zip(transforms.iter_mut()) { for (_, point) in element.point_domain.positions_mut() { diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index 3b08ac33d1..bb2202505e 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -3,13 +3,14 @@ use core::f64::consts::{PI, TAU}; use core::hash::{Hash, Hasher}; use core_types::blending::BlendMode; use core_types::bounds::{BoundingBox, RenderBoundingBox}; +use core_types::gpoll::Interrupt; use core_types::list::{ATTR_FILL, ATTR_STROKE, Item, ItemAttributeValues, List, ListDyn}; use core_types::registry::types::{Angle, Length, Multiplier, Percentage, PixelLength, Progression, SeedValue}; use core_types::transform::{Footprint, Transform}; use core_types::uuid::NodeId; use core_types::{ - ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_GRADIENT_TYPE, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_TRANSFORM, CloneVarArgs, - Color, Context, Ctx, ExtractAll, OwnedContextImpl, + ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_GRADIENT_TYPE, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_TRANSFORM, Color, Ctx, + DeriveCtx, }; use glam::{DAffine2, DMat2, DVec2}; use graphic_types::Vector; @@ -88,7 +89,7 @@ impl VectorListIterMut for List { /// Uniquely sets the fill and/or stroke style of every vector element to individual colors sampled along a chosen gradient. #[node_macro::node(category("Vector: Style"), path(graphene_core::vector))] -async fn assign_colors( +fn assign_colors( _: impl Ctx, /// The content with vector paths to apply the fill and/or stroke style to. #[implementations(List, List)] @@ -115,7 +116,7 @@ async fn assign_colors( repeat_every: u32, ) -> T where - T: VectorListIterMut + 'n + Send, + T: VectorListIterMut + Send, { let Some(row) = gradient.into_iter().next() else { return content }; @@ -156,7 +157,7 @@ where /// Applies a fill style to the vector content, giving an appearance to the area within the interior of the geometry. #[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("fill_properties"))] -async fn fill( +fn fill( _: impl Ctx, /// The content with vector paths to apply the fill style to. #[implementations( @@ -251,7 +252,7 @@ impl IntoF64Vec for String { /// Applies a stroke style to the vector content, giving an appearance to the area within the outline of the geometry. #[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("stroke_properties"))] -async fn stroke( +fn stroke( _: impl Ctx, /// The content with vector paths to apply the stroke style to. #[implementations( @@ -323,7 +324,7 @@ async fn stroke( dash_offset: f64, ) -> List where - List: VectorListIterMut + 'n + Send, + List: VectorListIterMut + Send, { let dash_lengths = dash_lengths.into_vec().into_iter().map(|length| length.max(0.)).collect(); @@ -356,7 +357,7 @@ where } #[node_macro::node(name("Copy to Points"), category("Repeat"), 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. @@ -440,7 +441,7 @@ async fn copy_to_points( } #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] -async fn round_corners( +fn round_corners( _: impl Ctx, source: List, #[hard(0..)] @@ -777,7 +778,7 @@ pub mod extrude_algorithms { } #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] -async fn extrude(_: impl Ctx, mut source: List, direction: DVec2, joining_algorithm: ExtrudeJoiningAlgorithm) -> List { +fn extrude(_: impl Ctx, mut source: List, direction: DVec2, joining_algorithm: ExtrudeJoiningAlgorithm) -> List { for vector in source.iter_element_values_mut() { extrude_algorithms::extrude(vector, direction, joining_algorithm); } @@ -785,7 +786,7 @@ async fn extrude(_: impl Ctx, mut source: List, direction: DVec2, joinin } #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] -async fn box_warp(_: impl Ctx, content: List, #[expose] rectangle: List) -> List { +fn box_warp(_: impl Ctx, content: List, #[expose] rectangle: List) -> List { let Some(target) = rectangle.element(0).cloned() else { return content }; let target_transform: DAffine2 = rectangle.attribute_cloned_or_default(ATTR_TRANSFORM, 0); @@ -870,7 +871,7 @@ fn bilinear_interpolate(t: DVec2, quad: &[DVec2; 4]) -> DVec2 { } #[node_macro::node(category("Vector"), path(graphene_core::vector))] -async fn pack_strips( +fn pack_strips( _: impl Ctx, #[implementations( List, @@ -991,7 +992,7 @@ where /// Automatically constructs tangents (Bézier handles) for anchor points in a vector path. #[node_macro::node(category("Vector: Modifier"), name("Auto-Tangents"), path(core_types::vector))] -async fn auto_tangents( +fn auto_tangents( _: impl Ctx, source: List, /// The amount of spread for the auto-tangents, from 0 (sharp corner) to 1 (full spread). @@ -1145,7 +1146,7 @@ async fn auto_tangents( } #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] -async fn bounding_box(_: impl Ctx, content: List) -> List { +fn bounding_box(_: impl Ctx, content: List) -> List { content .into_iter() .map(|mut row| { @@ -1170,7 +1171,7 @@ async fn bounding_box(_: impl Ctx, content: List) -> List { } #[node_macro::node(category("Vector: Measure"), path(core_types::vector))] -async fn dimensions(_: impl Ctx, content: List) -> DVec2 { +fn dimensions(_: impl Ctx, content: List) -> DVec2 { (0..content.len()) .filter_map(|index| content.element(index).unwrap().bounding_box_with_transform(content.attribute_cloned_or_default(ATTR_TRANSFORM, index))) .reduce(|[acc_top_left, acc_bottom_right], [top_left, bottom_right]| [acc_top_left.min(top_left), acc_bottom_right.max(bottom_right)]) @@ -1186,7 +1187,7 @@ fn as_vector(_: impl Ctx, value: List) -> List { /// Creates a polyline from a series of vector points, replacing any existing segments and regions that may already exist. #[node_macro::node(category("Vector"), name("Points to Polyline"), path(core_types::vector))] -async fn points_to_polyline(_: impl Ctx, mut points: List, #[default(true)] closed: bool) -> List { +fn points_to_polyline(_: impl Ctx, mut points: List, #[default(true)] closed: bool) -> List { for vector in points.iter_element_values_mut() { let mut segment_domain = SegmentDomain::new(); let mut next_id = SegmentId::ZERO; @@ -1214,7 +1215,7 @@ async fn points_to_polyline(_: impl Ctx, mut points: List, #[default(tru } #[node_macro::node(category("Vector: Modifier"), path(core_types::vector), properties("offset_path_properties"))] -async fn offset_path(_: impl Ctx, content: List, distance: f64, join: StrokeJoin, #[default(4.)] miter_limit: f64) -> List { +fn offset_path(_: impl Ctx, content: List, distance: f64, join: StrokeJoin, #[default(4.)] miter_limit: f64) -> List { content .into_iter() .map(|mut row| { @@ -1258,7 +1259,7 @@ async fn offset_path(_: impl Ctx, content: List, distance: f64, join: St } #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] -async fn solidify_stroke(_: impl Ctx, #[implementations(List, List)] content: T) -> List { +fn solidify_stroke(_: impl Ctx, #[implementations(List, List)] content: T) -> List { // TODO: Make this node support stroke align, which it currently ignores let graphic_list = content.into_graphic_list(); @@ -1366,7 +1367,7 @@ async fn solidify_stroke(_: impl Ctx, #[implementations(List } #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] -async fn separate_subpaths(_: impl Ctx, content: List) -> List { +fn separate_subpaths(_: impl Ctx, content: List) -> List { content .into_iter() .flat_map(|row| { @@ -1397,7 +1398,7 @@ async fn separate_subpaths(_: impl Ctx, content: List) -> List { /// Determines if the subpath at the given index (across all vector element subpaths) is closed, meaning its ends are connected together forming a loop. #[node_macro::node(name("Path is Closed"), category("Vector: Measure"), path(core_types::vector))] -async fn path_is_closed( +fn path_is_closed( _: impl Ctx, /// The vector content whose subpaths are inspected. content: List, @@ -1412,25 +1413,25 @@ async fn path_is_closed( } #[node_macro::node(category("Vector"), path(graphene_core::vector))] -async fn map_points(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: List, mapped: impl Node, Output = DVec2>) -> List { +fn map_points(ctx: impl Ctx + DeriveCtx, content: List, mapped: impl Node, Output = DVec2>) -> Result, Interrupt> { + let spilled = ctx.index_head(); let mut content = content; let mut index = 0; for vector in content.iter_element_values_mut() { for (_, position) in vector.point_domain.positions_mut() { - let owned_ctx = OwnedContextImpl::from(ctx.clone()).with_index(index).with_position(*position); + let scoped = ctx.push_position(*position); + *position = mapped.eval(&scoped.ctx().promoted(&spilled, index))?; index += 1; - - *position = mapped.eval(owned_ctx.into_context()).await; } } - content + Ok(content) } // TODO: Rename to "Combine Paths" and make this happen per-element instead of flattening every element into a single path. The migration for this should then become a Flatten Vector -> Combine Paths pair of nodes. #[node_macro::node(category("Vector"), path(graphene_core::vector))] -pub async fn flatten_path(_: impl Ctx, #[implementations(List, List)] content: T) -> List { +pub fn flatten_path(_: impl Ctx, #[implementations(List, List)] content: T) -> List { let graphic_list = content.into_graphic_list(); let flattened = graphic_list.clone().into_flattened_list::(); @@ -1486,7 +1487,7 @@ pub async fn flatten_path(_: impl Ctx, #[implementations(Lis /// Convert vector geometry into a polyline composed of evenly spaced points. #[node_macro::node(category("Vector: Modifier"), path(core_types::vector), properties("sample_polyline_properties"), memoize)] -async fn sample_polyline( +fn sample_polyline( _: impl Ctx, content: List, spacing: PointSpacingType, @@ -1572,7 +1573,7 @@ async fn sample_polyline( /// Simplifies vector paths by reducing the number of curve segments while preserving the overall shape within the given tolerance. #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] -async fn simplify( +fn simplify( _: impl Ctx, /// The vector paths to simplify. content: List, @@ -1616,7 +1617,7 @@ async fn simplify( /// Decimates vector paths into polylines by sampling any curves into line segments, then removing points that don't significantly contribute to the shape using the Ramer-Douglas-Peucker algorithm. #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] -async fn decimate( +fn decimate( _: impl Ctx, /// The vector paths to decimate. content: List, @@ -1744,7 +1745,7 @@ async fn decimate( /// /// If multiple subpaths make up the path, the whole number part of the progression value selects the subpath and the decimal part determines the position along it. #[node_macro::node(category("Vector: Modifier"), path(graphene_core::vector))] -async fn cut_path( +fn cut_path( _: impl Ctx, /// The path to insert a cut into. mut content: List, @@ -1795,7 +1796,7 @@ async fn cut_path( /// Cuts path segments into separate disconnected pieces where each is a distinct subpath. #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] -async fn cut_segments(_: impl Ctx, mut content: List) -> List { +fn cut_segments(_: impl Ctx, mut content: List) -> List { // 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 for vector in content.iter_element_values_mut() { let points_count = vector.point_domain.ids().len(); @@ -1854,7 +1855,7 @@ async fn cut_segments(_: impl Ctx, mut content: List) -> List { /// /// If multiple subpaths make up the path, the whole number part of the progression value selects the subpath and the decimal part determines the position along it. #[node_macro::node(name("Position on Path"), category("Vector: Measure"), path(graphene_core::vector))] -async fn position_on_path( +fn position_on_path( _: impl Ctx, /// The path to traverse. content: List, @@ -1892,7 +1893,7 @@ async fn position_on_path( /// /// If multiple subpaths make up the path, the whole number part of the progression value selects the subpath and the decimal part determines the position along it. #[node_macro::node(name("Tangent on Path"), category("Vector: Measure"), path(graphene_core::vector))] -async fn tangent_on_path( +fn tangent_on_path( _: impl Ctx, /// The path to traverse. content: List, @@ -1940,7 +1941,7 @@ async fn tangent_on_path( } #[node_macro::node(category("Vector: Modifier"), path(core_types::vector), memoize)] -async fn scatter_points( +fn scatter_points( _: impl Ctx, content: List, #[unit(" px")] @@ -1990,7 +1991,7 @@ async fn scatter_points( } #[node_macro::node(name("Spline"), category("Vector: Modifier"), path(core_types::vector))] -async fn spline(_: impl Ctx, content: List) -> List { +fn spline(_: impl Ctx, content: List) -> List { content .into_iter() .filter_map(|mut row| { @@ -2090,7 +2091,7 @@ fn apply_point_deltas(element: &mut Vector, deltas: &[DVec2], transform: DAffine /// Perturbs the positions of anchor points in vector geometry by random amounts and directions. #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] -async fn jitter_points( +fn jitter_points( _: impl Ctx, /// The vector geometry with points to be jittered. content: List, @@ -2140,7 +2141,7 @@ async fn jitter_points( /// Displaces anchor points along their normal direction (perpendicular to the path) by a set distance. /// Points with 0 or 3+ segment connections have no well-defined normal and are left in place. #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] -async fn offset_points( +fn offset_points( _: impl Ctx, /// The vector geometry with points to be offset. content: List, @@ -2177,7 +2178,7 @@ async fn offset_points( /// /// *Progression* morphs through all objects. Interpolation is linear unless *Path* geometry is provided to control the trajectory between key objects. The **Origins to Polyline** node may be used to create a path with anchor points corresponding to each object. Other nodes can modify its path segments. #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] -async fn morph( +fn morph( _: impl Ctx, /// The vector objects to interpolate between. Mixed graphic content is deeply flattened to keep only vector elements. #[implementations(List, List)] @@ -3124,19 +3125,19 @@ fn point_inside(_: impl Ctx, source: List, point: DVec2) -> bool { // TODO: Return u32, u64, or usize instead of f64 after #1621 is resolved and has allowed us to implement automatic type conversion in the node graph for nodes with generic type inputs. // TODO: (Currently automatic type conversion only works for concrete types, via the Graphene preprocessor and not the full Graphene type system.) #[node_macro::node(category("General"), path(graphene_core::vector))] -async fn count_elements(_: impl Ctx, content: ListDyn) -> f64 { +fn count_elements(_: impl Ctx, content: ListDyn) -> f64 { content.len() as f64 } #[node_macro::node(category("Vector: Measure"), path(graphene_core::vector))] -async fn count_points(_: impl Ctx, content: List) -> f64 { +fn count_points(_: impl Ctx, content: List) -> f64 { content.iter_element_values().map(|vector| vector.point_domain.positions().len() as f64).sum() } /// Retrieves the vec2 position (in local space) of the anchor point at the specified index in a `List` of vector elements. /// If no value exists at that index, the position (0, 0) is returned. #[node_macro::node(category("Vector: Measure"), path(graphene_core::vector))] -async fn index_points( +fn index_points( _: impl Ctx, /// The vector element or elements containing the anchor points to be retrieved. content: List, @@ -3170,7 +3171,7 @@ async fn index_points( } #[node_macro::node(category("Vector: Measure"), path(core_types::vector))] -async fn path_length(_: impl Ctx, source: List) -> f64 { +fn path_length(_: impl Ctx, source: List) -> f64 { (0..source.len()) .map(|index| { let transform: DAffine2 = source.attribute_cloned_or_default(ATTR_TRANSFORM, index); @@ -3189,26 +3190,24 @@ async fn path_length(_: impl Ctx, source: List) -> f64 { } #[node_macro::node(category("Vector: Measure"), path(core_types::vector))] -async fn area(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: impl Node, Output = List>) -> f64 { - let new_ctx = OwnedContextImpl::from(ctx).with_footprint(Footprint::default()).into_context(); - let vector = content.eval(new_ctx).await; +fn area(ctx: impl Ctx + DeriveCtx, content: impl Node, Output = List>) -> Result { + let vector = content.eval(&ctx.with_footprint(&Footprint::DEFAULT))?; - (0..vector.len()) + Ok((0..vector.len()) .map(|index| { let transform: DAffine2 = vector.attribute_cloned_or_default(ATTR_TRANSFORM, index); let area_scale = transform.matrix2.determinant().abs(); vector.element(index).unwrap().stroke_bezpath_iter().map(|subpath| subpath.area() * area_scale).sum::() }) - .sum() + .sum()) } #[node_macro::node(category("Vector: Measure"), path(core_types::vector))] -async fn centroid(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: impl Node, Output = List>, centroid_type: CentroidType) -> DVec2 { - let new_ctx = OwnedContextImpl::from(ctx).with_footprint(Footprint::default()).into_context(); - let vector = content.eval(new_ctx).await; +fn centroid(ctx: impl Ctx + DeriveCtx, content: impl Node, Output = List>, centroid_type: CentroidType) -> Result { + let vector = content.eval(&ctx.with_footprint(&Footprint::DEFAULT))?; if vector.is_empty() { - return DVec2::ZERO; + return Ok(DVec2::ZERO); } // All subpath centroid positions added together as if they were vectors from the origin. @@ -3234,7 +3233,7 @@ async fn centroid(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: impl Node< } if sum > 0. { - centroid / sum + Ok(centroid / sum) } // Without a summed denominator, return the average of all positions instead else { @@ -3255,31 +3254,17 @@ async fn centroid(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: impl Node< .inspect(|_| count += 1) .sum::(); - if count != 0 { summed_positions / (count as f64) } else { DVec2::ZERO } + if count != 0 { Ok(summed_positions / (count as f64)) } else { Ok(DVec2::ZERO) } } } #[cfg(test)] mod test { use super::*; - use core_types::Node; use kurbo::{CubicBez, Ellipse, Point, Rect}; - use std::future::Future; - use std::pin::Pin; use vector_types::vector::algorithms::bezpath_algorithms::{TValue, trim_pathseg}; use vector_types::vector::misc::pathseg_abs_diff_eq; - #[derive(Clone)] - pub struct FutureWrapperNode(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 }) - } - } - fn vector_node_from_bezpath(bezpath: BezPath) -> List { List::new_from_element(Vector::from_bezpath(bezpath)) } @@ -3290,9 +3275,9 @@ mod test { Item::new_from_element(row).with_attribute(ATTR_TRANSFORM, transform) } - #[tokio::test] - async fn bounding_box() { - let bounding_box = super::bounding_box((), vector_node_from_bezpath(Rect::new(-1., -1., 1., 1.).to_path(DEFAULT_ACCURACY))).await; + #[test] + fn bounding_box() { + let bounding_box = super::bounding_box(&(), vector_node_from_bezpath(Rect::new(-1., -1., 1., 1.).to_path(DEFAULT_ACCURACY))); let bounding_box = bounding_box.element(0).unwrap(); assert_eq!(bounding_box.region_manipulator_groups().count(), 1); let manipulator_groups_anchors = bounding_box @@ -3310,7 +3295,7 @@ mod test { let square = Vector::from_bezpath(Rect::new(-1., -1., 1., 1.).to_path(DEFAULT_ACCURACY)); let mut square = List::new_from_element(square); square.with_attribute_mut_or_default(ATTR_TRANSFORM, 0, |t: &mut DAffine2| *t *= DAffine2::from_angle(std::f64::consts::FRAC_PI_4)); - let bounding_box = BoundingBoxNode { content: FutureWrapperNode(square) }.eval(Footprint::default()).await; + let bounding_box = super::bounding_box(&(), square); let bounding_box = bounding_box.element(0).unwrap(); assert_eq!(bounding_box.region_manipulator_groups().count(), 1); let manipulator_groups_anchors = bounding_box @@ -3327,15 +3312,15 @@ 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); let expected_points = Vector::from_bezpath(points.clone()).point_domain.positions().to_vec(); - let copy_to_points = super::copy_to_points(Footprint::default(), vector_node_from_bezpath(points), vector_node_from_bezpath(element), 1., 1., 0., 0, 0., 0).await; - let flatten_path = super::flatten_path(Footprint::default(), copy_to_points).await; + let copy_to_points = super::copy_to_points(&Footprint::default(), vector_node_from_bezpath(points), vector_node_from_bezpath(element), 1., 1., 0., 0, 0., 0); + let flatten_path = super::flatten_path(&Footprint::default(), copy_to_points); let flattened_copy_to_points = flatten_path.element(0).unwrap(); assert_eq!(flattened_copy_to_points.region_manipulator_groups().count(), expected_points.len()); @@ -3350,35 +3335,34 @@ 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(), vector_node_from_bezpath(path), PointSpacingType::Separation, 30., 0, 0., 0., false).await; + let sample_polyline = super::sample_polyline(&Footprint::default(), vector_node_from_bezpath(path), PointSpacingType::Separation, 30., 0, 0., 0., false); let sample_polyline = sample_polyline.element(0).unwrap(); assert_eq!(sample_polyline.point_domain.positions().len(), 4); for (pos, expected) in sample_polyline.point_domain.positions().iter().zip([DVec2::X * 0., DVec2::X * 30., DVec2::X * 60., DVec2::X * 90.]) { 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(), vector_node_from_bezpath(path), PointSpacingType::Separation, 18., 0, 45., 10., true).await; + let sample_polyline = super::sample_polyline(&Footprint::default(), vector_node_from_bezpath(path), PointSpacingType::Separation, 18., 0, 45., 10., true); let sample_polyline = sample_polyline.element(0).unwrap(); assert_eq!(sample_polyline.point_domain.positions().len(), 4); for (pos, expected) in sample_polyline.point_domain.positions().iter().zip([DVec2::X * 45., DVec2::X * 60., DVec2::X * 75., DVec2::X * 90.]) { 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(), + &Footprint::default(), vector_node_from_bezpath(Ellipse::from_rect(Rect::new(-50., -50., 50., 50.)).to_path(DEFAULT_ACCURACY)), 10. * std::f64::consts::SQRT_2, 0, - ) - .await; + ); let poisson_points = poisson_points.element(0).unwrap(); assert!( (20..=40).contains(&poisson_points.point_domain.positions().len()), @@ -3389,33 +3373,33 @@ mod test { assert!(point.length() < 50. + 1., "Expected point in circle {point}") } } - #[tokio::test] - async fn path_length() { + #[test] + fn path_length() { let bezpath = Rect::new(100., 100., 201., 201.).to_path(DEFAULT_ACCURACY); let transform = DAffine2::from_scale(DVec2::new(2., 2.)); let row = create_vector_item(bezpath, transform); let list = (0..5).map(|_| row.clone()).collect::>(); - let length = super::path_length(Footprint::default(), list).await; + let length = super::path_length(&Footprint::default(), list); // 101 (each rectangle edge length) * 4 (rectangle perimeter) * 2 (scale) * 5 (number of rows) assert_eq!(length, 101. * 4. * 2. * 5.); } - #[tokio::test] - async fn spline() { - let spline = super::spline(Footprint::default(), vector_node_from_bezpath(Rect::new(0., 0., 100., 100.).to_path(DEFAULT_ACCURACY))).await; + #[test] + fn spline() { + let spline = super::spline(&Footprint::default(), vector_node_from_bezpath(Rect::new(0., 0., 100., 100.).to_path(DEFAULT_ACCURACY))); let spline = spline.element(0).unwrap(); assert_eq!(spline.stroke_bezpath_iter().count(), 1); assert_eq!(spline.point_domain.positions(), &[DVec2::ZERO, DVec2::new(100., 0.), DVec2::new(100., 100.), DVec2::new(0., 100.)]); } - #[tokio::test] - async fn morph() { + #[test] + fn morph() { let mut rectangles = vector_node_from_bezpath(Rect::new(0., 0., 100., 100.).to_path(DEFAULT_ACCURACY)); let mut second_rectangle = rectangles.clone_item(0).unwrap(); *second_rectangle.attribute_mut_or_insert_default::(ATTR_TRANSFORM) *= DAffine2::from_translation((-100., -100.).into()); rectangles.push(second_rectangle); - let morphed = super::morph(Footprint::default(), rectangles, 0.5, false, InterpolationDistribution::default(), List::default()).await; + let morphed = super::morph(&Footprint::default(), rectangles, 0.5, false, InterpolationDistribution::default(), List::default()); let morphed_element = morphed.element(0).unwrap(); // Geometry stays in local space (original rectangle coordinates) assert_eq!( @@ -3426,8 +3410,8 @@ mod test { assert!((morphed.attribute_cloned_or_default::(ATTR_TRANSFORM, 0).translation - DVec2::new(-50., -50.)).length() < 1e-3); } - #[tokio::test] - async fn morph_interpolates_fill() { + #[test] + fn morph_interpolates_fill() { let rect = || { let mut v = Vector::default(); v.append_bezpath(Rect::new(0., 0., 100., 100.).to_path(DEFAULT_ACCURACY)); @@ -3444,7 +3428,7 @@ mod test { let mut content = List::new_from_item(item_a); content.push(item_b); - let morphed = super::morph(Footprint::default(), content, 0.5, false, InterpolationDistribution::default(), List::default()).await; + let morphed = super::morph(&Footprint::default(), content, 0.5, false, InterpolationDistribution::default(), List::default()); let fill = graphic_list_at(&morphed, 0, ATTR_FILL).expect("Morph should keep the fill paint at the midpoint"); @@ -3471,10 +3455,10 @@ 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_node_from_bezpath(source), 2_f64.sqrt() * 10.); + let beveled = super::bevel(&Footprint::default(), vector_node_from_bezpath(source), 2_f64.sqrt() * 10.); let beveled = beveled.element(0).unwrap(); assert_eq!(beveled.point_domain.positions().len(), 8); @@ -3493,8 +3477,8 @@ mod test { contains_segment(beveled.clone(), PathSeg::Line(Line::new(Point::new(10., 100.), Point::new(0., 90.)))); } - #[tokio::test] - async fn bevel_open_curve() { + #[test] + fn bevel_open_curve() { let curve = PathSeg::Cubic(CubicBez::new(Point::ZERO, Point::new(10., 0.), Point::new(10., 100.), Point::new(100., 0.))); let mut source = BezPath::new(); @@ -3502,7 +3486,7 @@ mod test { source.line_to(Point::ZERO); source.push(curve.as_path_el()); - let beveled = super::bevel((), vector_node_from_bezpath(source), 2_f64.sqrt() * 10.); + let beveled = super::bevel(&(), vector_node_from_bezpath(source), 2_f64.sqrt() * 10.); let beveled = beveled.element(0).unwrap(); assert_eq!(beveled.point_domain.positions().len(), 4); @@ -3517,8 +3501,8 @@ mod test { contains_segment(beveled.clone(), PathSeg::Line(Line::new(Point::new(-8.2, 0.), trimmed.start()))); } - #[tokio::test] - async fn bevel_with_transform() { + #[test] + fn bevel_with_transform() { let curve = PathSeg::Cubic(CubicBez::new(Point::ZERO, Point::new(10., 0.), Point::new(10., 100.), Point::new(100., 0.))); let mut source = BezPath::new(); @@ -3531,7 +3515,7 @@ mod test { vector_list.set_attribute(ATTR_TRANSFORM, 0, DAffine2::from_scale_angle_translation(DVec2::splat(10.), 1., DVec2::new(99., 77.))); - let beveled = super::bevel((), List::new_from_element(vector), 2_f64.sqrt() * 10.); + let beveled = super::bevel(&(), List::new_from_element(vector), 2_f64.sqrt() * 10.); let beveled = beveled.element(0).unwrap(); assert_eq!(beveled.point_domain.positions().len(), 4); @@ -3546,15 +3530,15 @@ mod test { contains_segment(beveled.clone(), PathSeg::Line(Line::new(Point::new(-8.2, 0.), trimmed.start()))); } - #[tokio::test] - async fn bevel_too_high() { + #[test] + fn bevel_too_high() { let mut source = BezPath::new(); source.move_to(Point::ZERO); source.line_to(Point::new(100., 0.)); source.line_to(Point::new(100., 100.)); source.line_to(Point::new(0., 100.)); - let beveled = super::bevel(Footprint::default(), vector_node_from_bezpath(source), 999.); + let beveled = super::bevel(&Footprint::default(), vector_node_from_bezpath(source), 999.); let beveled = beveled.element(0).unwrap(); assert_eq!(beveled.point_domain.positions().len(), 6); @@ -3570,15 +3554,15 @@ mod test { contains_segment(beveled.clone(), PathSeg::Line(Line::new(Point::new(100., 50.), Point::new(50., 100.)))); } - #[tokio::test] - async fn bevel_repeated_point() { + #[test] + fn bevel_repeated_point() { let line = PathSeg::Line(Line::new(Point::ZERO, Point::new(100., 0.))); let point = PathSeg::Cubic(CubicBez::new(Point::new(100., 0.), Point::ZERO, Point::ZERO, Point::new(100., 0.))); let curve = PathSeg::Cubic(CubicBez::new(Point::new(100., 0.), Point::new(110., 0.), Point::new(110., 200.), Point::new(200., 0.))); let subpath = BezPath::from_path_segments([line, point, curve].into_iter()); - let beveled_list = super::bevel(Footprint::default(), vector_node_from_bezpath(subpath), 5.); + let beveled_list = super::bevel(&Footprint::default(), vector_node_from_bezpath(subpath), 5.); let beveled = beveled_list.element(0).unwrap(); assert_eq!(beveled.point_domain.positions().len(), 6); diff --git a/node-graph/preprocessor/src/lib.rs b/node-graph/preprocessor/src/lib.rs index 4e543a7b7a..200839afe8 100644 --- a/node-graph/preprocessor/src/lib.rs +++ b/node-graph/preprocessor/src/lib.rs @@ -116,13 +116,19 @@ impl Preprocessor { for (id, metadata) in core_types::registry::NODE_METADATA.lock().unwrap().iter() { let id = id.clone(); - let NodeMetadata { fields, memoize, inject_scope, .. } = metadata; + let NodeMetadata { + fields, + memoize, + inject_scope, + async_source_fields, + .. + } = metadata; 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() }); + let valid_call_args: HashSet<_> = implementations.iter().map(|entry| entry.io.call_argument.clone()).collect(); + let first_node_io = implementations.first().map(|entry| &entry.io).unwrap_or(const { &NodeIOTypes::empty() }); let mut node_io_types = vec![HashSet::new(); fields.len()]; - for (_, node_io) in implementations.iter() { - for (i, ty) in node_io.inputs.iter().enumerate() { + for entry in implementations.iter() { + for (i, ty) in entry.io.inputs.iter().enumerate() { node_io_types[i].insert(ty.clone()); } } @@ -131,16 +137,20 @@ impl Preprocessor { input_type = &const { generic!(D) }; } - let inputs: Vec<_> = node_inputs(fields, first_node_io); - let input_count = inputs.len(); - let network_inputs = (0..input_count).map(|i| NodeInput::node(NodeId(i as u64), 0)).collect(); + let mut inputs: Vec<_> = node_inputs(fields, first_node_io); + let wrapper_input_count = inputs.len() - if *async_source_fields { 2 } else { 0 }; + + // The injected fields must not surface as wrapper inputs, and the `_source` reflection must sit on + // the kernel itself so the source id lands on a node that survives flattening. + let injected_inputs = inputs.split_off(wrapper_input_count); + let network_inputs = (0..wrapper_input_count).map(|i| NodeInput::node(NodeId(i as u64), 0)).chain(injected_inputs).collect(); let passthrough_node = ops::passthrough::IDENTIFIER; let mut generated_nodes = 0; let mut nodes: HashMap<_, _, _> = node_io_types .iter() - .take(input_count) + .take(wrapper_input_count) .enumerate() .map(|(i, inputs)| { ( @@ -185,7 +195,7 @@ impl Preprocessor { }) .collect(); - if generated_nodes == 0 && !memoize && !inject_scope { + if generated_nodes == 0 && !memoize && !inject_scope && !async_source_fields { continue; } @@ -199,13 +209,14 @@ impl Preprocessor { ..Default::default() }; - nodes.insert(NodeId(input_count as u64), document_node); + let main_node_id = NodeId(wrapper_input_count as u64); + nodes.insert(main_node_id, document_node); // If memoize is requested, append a Memoize node after the main node and redirect the export through it let export_node_id = if *memoize { - let memoize_node_id = NodeId(input_count as u64 + 1); + let memoize_node_id = NodeId(wrapper_input_count as u64 + 1); let memoize_node = DocumentNode { - inputs: vec![NodeInput::node(NodeId(input_count as u64), 0)], + inputs: vec![NodeInput::node(main_node_id, 0)], implementation: DocumentNodeImplementation::ProtoNode(graphene_core::memo::memoize::IDENTIFIER.clone()), visible: true, ..Default::default() @@ -213,7 +224,7 @@ impl Preprocessor { nodes.insert(memoize_node_id, memoize_node); memoize_node_id } else { - NodeId(input_count as u64) + main_node_id }; let node = DocumentNode { @@ -238,7 +249,7 @@ impl Preprocessor { // If `inject_scope` is requested, prepare the proto node template and type info needed if *inject_scope && let Some(implementations) = node_registry.get(&id) - && let Some((_, node_io)) = implementations.first() + && let Some(node_io) = implementations.first().map(|entry| &entry.io) { let template = DocumentNode { inputs: node_inputs(fields, node_io), @@ -279,6 +290,7 @@ pub fn node_inputs(fields: &[registry::FieldMetadata], first_node_io: &NodeIOTyp } } RegistryValueSource::Scope(data) => return NodeInput::scope(*data), + RegistryValueSource::SourceId => return NodeInput::Reflection(DocumentNodeMetadata::SourceId), }; if let Some(type_default) = TaggedValue::from_type(ty) { diff --git a/node-graph/rfcs/fine-grained-context-caching.md b/node-graph/rfcs/fine-grained-context-caching.md index 13a79f571b..c28b3e5c57 100644 --- a/node-graph/rfcs/fine-grained-context-caching.md +++ b/node-graph/rfcs/fine-grained-context-caching.md @@ -126,6 +126,12 @@ pub trait ModifyIndex: ExtractIndex + InjectIndex {} pub trait ModifyVarArgs: ExtractVarArgs + InjectVarArgs {} ``` +### Authoring rule: forward with Modify*, consume with Extract* + +Declare a feature via `Modify*` when the node only reads it to compute a new value it injects for its children (a "forward"), and via `Extract*` only when the node genuinely consumes the value for its own output (a "sink"). + +The analysis skips `Modify*` bounds but treats every `Extract*` bound as an *unconditional* requirement. Because `Modify*` is a supertrait of `Extract*`, a `Modify*` bound already grants the read capability (e.g. `modify_footprint`, which is `where Self: ExtractFootprint`), so it is a mistake to list both. Writing `impl Ctx + ExtractFootprint + ModifyFootprint` on a forwarding node re-introduces the feature as a hard dependency at every such node, which propagates up the whole tree and pins upstream memos to a value the node never actually consumes (e.g. a viewport pan invalidating a render cache that renders in local space). Use `impl Ctx + ModifyFootprint` alone. + ### Conditional Context Dependencies Modify* traits represent a special case in context analysis: diff --git a/tools/node-docs/src/page_category.rs b/tools/node-docs/src/page_category.rs index e16a50723d..ae6dda791c 100644 --- a/tools/node-docs/src/page_category.rs +++ b/tools/node-docs/src/page_category.rs @@ -76,15 +76,16 @@ fn write_nodes_table_rows(page: &mut std::fs::File, nodes: &[(&core_types::Proto let implementations = node_registry.get(id)?; let valid_primary_inputs_to_outputs = implementations .iter() - .map(|(_, node_io)| { - let input = node_io + .map(|entry| { + let input = entry + .io .inputs .first() .map(|ty| ty.nested_type()) .filter(|&ty| ty != &concrete!(())) .map(ToString::to_string) .unwrap_or_default(); - let output = node_io.return_value.nested_type().to_string(); + let output = entry.io.return_value.nested_type().to_string(); format!("`{input} → {output}`") }) .collect::>(); diff --git a/tools/node-docs/src/page_node.rs b/tools/node-docs/src/page_node.rs index e4a9962020..df4a868a80 100644 --- a/tools/node-docs/src/page_node.rs +++ b/tools/node-docs/src/page_node.rs @@ -23,8 +23,8 @@ pub fn write_node_page(index: usize, id: &core_types::ProtoNodeIdentifier, metad // Input types let mut valid_input_types = vec![Vec::new(); metadata.fields.len()]; - for (_, node_io) in implementations.iter() { - for (i, ty) in node_io.inputs.iter().enumerate() { + for entry in implementations.iter() { + for (i, ty) in entry.io.inputs.iter().enumerate() { valid_input_types[i].push(ty.nested_type().clone()); } } @@ -35,7 +35,7 @@ pub fn write_node_page(index: usize, id: &core_types::ProtoNodeIdentifier, metad } // Primary output types - let valid_primary_outputs = implementations.iter().map(|(_, node_io)| node_io.return_value.nested_type().clone()).collect::>(); + let valid_primary_outputs = implementations.iter().map(|entry| entry.io.return_value.nested_type().clone()).collect::>(); // Write sections to the file write_frontmatter(&mut page, metadata, index + 1);