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/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index 804ccb36d8..36f7b87d70 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -16,6 +16,7 @@ use graphene_std::ops::{Convert, ConvertAsync}; use graphene_std::platform_application_io::canvas_utils::{Canvas, CanvasSurface, CanvasSurfaceHandle}; use graphene_std::raster_types::Raster; use graphene_std::renderer::{Render, RenderParams, RenderSvgSegmentList, SvgRender, SvgSegment}; +use graphene_std::runtime::{DynGraphRuntime, DynSpawner, GraphRuntime, NoopSpawner, RuntimeHandle}; use graphene_std::transform::RenderQuality; use graphene_std::vector::Vector; use graphene_std::vector::style::RenderMode; @@ -120,8 +121,12 @@ pub static NODE_RUNTIME: once_cell::sync::Lazy>> = onc impl NodeRuntime { pub fn new(receiver: Receiver, sender: Sender) -> Self { + let graph_runtime: Arc = Arc::new(GraphRuntime::new(Box::new(NoopSpawner) as Box)); + 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(), @@ -132,6 +137,7 @@ impl NodeRuntime { 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, @@ -202,6 +208,7 @@ 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() { @@ -575,6 +582,7 @@ impl NodeRuntime { 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/node-graph/graph-craft/src/proto.rs b/node-graph/graph-craft/src/proto.rs index 0de07b6102..f7ae796008 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> { diff --git a/node-graph/graphene-cli/src/main.rs b/node-graph/graphene-cli/src/main.rs index e13f619918..24d036123b 100644 --- a/node-graph/graphene-cli/src/main.rs +++ b/node-graph/graphene-cli/src/main.rs @@ -14,6 +14,7 @@ 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, NoopSpawner, RuntimeHandle}; use interpreted_executor::dynamic_executor::DynamicExecutor; use interpreted_executor::util::wrap_network_in_scope; use std::error::Error; @@ -182,10 +183,12 @@ async fn main() -> Result<(), Box> { let preferences = EditorPreferences { max_render_region_size: EditorPreferences::default().max_render_region_size, }; + let graph_runtime: Arc = Arc::new(GraphRuntime::new(Box::new(NoopSpawner) as Box)); let editor_api = Arc::new(PlatformEditorApi { application_io: Some(application_io_for_api), node_graph_message_sender: Box::new(UpdateLogger {}), editor_preferences: Box::new(preferences), + runtime: RuntimeHandle(graph_runtime.clone()), }); let proto_graph = compile_graph(node_network, editor_api, gdd.as_ref())?; @@ -218,7 +221,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()); @@ -285,7 +288,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 = 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())?; + executor.set_runtime(runtime); Ok(executor) } diff --git a/node-graph/interpreted-executor/src/dynamic_executor.rs b/node-graph/interpreted-executor/src/dynamic_executor.rs index cc8674214b..589820f482 100644 --- a/node-graph/interpreted-executor/src/dynamic_executor.rs +++ b/node-graph/interpreted-executor/src/dynamic_executor.rs @@ -4,7 +4,7 @@ use core_types::context::{ContextImpl, DynSlot, EvalScope, VarArg, VarArgLink, V use core_types::gnode::GNode; use core_types::gpoll::GPoll; use core_types::registry::{EdgeHandle, ErasedGNode}; -use core_types::runtime::{GraphRuntime, SourceFuture, Spawner}; +use core_types::runtime::{DynGraphRuntime, DynSpawner, GraphRuntime, NoopSpawner}; use graph_craft::Type; use graph_craft::document::NodeId; use graph_craft::document::value::TaggedValue; @@ -17,15 +17,6 @@ use std::sync::{Arc, Mutex, PoisonError}; const ARENA_CAPACITY: usize = 1 << 20; -/// Dropped tasks never complete. -pub struct NoopSpawner; - -impl Spawner for NoopSpawner { - fn spawn(&self, _task: SourceFuture) { - log::warn!("async source spawned before a host spawner is wired; the task is dropped"); - } -} - /// An executor of a node graph that does not require an online compilation server, and instead uses `Box`. pub struct DynamicExecutor { output: NodeId, @@ -36,9 +27,15 @@ pub struct DynamicExecutor { // This allows us to keep the nodes around for one more frame which is used for introspection orphaned_nodes: HashSet, arena: Mutex, - runtime: Arc>, + runtime: Arc, + live_sources: Vec, } +fn noop_runtime() -> Arc { + Arc::new(GraphRuntime::new(Box::new(NoopSpawner) as Box)) +} + + impl Default for DynamicExecutor { fn default() -> Self { Self { @@ -47,7 +44,8 @@ impl Default for DynamicExecutor { typing_context: TypingContext::new(&node_registry::NODE_REGISTRY), orphaned_nodes: HashSet::new(), arena: Mutex::new(Arena::new(ARENA_CAPACITY)), - runtime: Arc::new(GraphRuntime::new(NoopSpawner)), + runtime: noop_runtime(), + live_sources: Vec::new(), } } } @@ -71,7 +69,10 @@ impl DynamicExecutor { let mut typing_context = TypingContext::new(&node_registry::NODE_REGISTRY); typing_context.update(&proto_network)?; let output = proto_network.output; + let sources = proto_network.source_ids(); let tree = BorrowTree::new(proto_network, &typing_context).await?; + let runtime = noop_runtime(); + runtime.retain_sources(&sources); Ok(Self { tree, @@ -79,10 +80,20 @@ impl DynamicExecutor { typing_context, orphaned_nodes: HashSet::new(), arena: Mutex::new(Arena::new(ARENA_CAPACITY)), - runtime: Arc::new(GraphRuntime::new(NoopSpawner)), + 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 { @@ -108,11 +119,14 @@ impl DynamicExecutor { (ResolvedDocumentNodeTypesDelta { add, remove: Vec::new() }, e) })?; + let sources = proto_network.source_ids(); let (add, orphaned) = self .tree .update(proto_network, &self.typing_context) .await .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 { diff --git a/node-graph/libraries/application-io/src/lib.rs b/node-graph/libraries/application-io/src/lib.rs index 75f6ba1dcc..80a2012b17 100644 --- a/node-graph/libraries/application-io/src/lib.rs +++ b/node-graph/libraries/application-io/src/lib.rs @@ -112,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 {} @@ -122,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/runtime.rs b/node-graph/libraries/core-types/src/runtime.rs index ed7e5df55e..f206045753 100644 --- a/node-graph/libraries/core-types/src/runtime.rs +++ b/node-graph/libraries/core-types/src/runtime.rs @@ -36,6 +36,34 @@ pub trait Spawner { fn spawn(&self, task: SourceFuture); } +#[cfg(not(target_family = "wasm"))] +pub type DynSpawner = dyn Spawner + Send + Sync; +#[cfg(target_family = "wasm")] +pub type DynSpawner = dyn Spawner; + +impl Spawner for Box { + fn spawn(&self, task: SourceFuture) { + (**self).spawn(task) + } +} + +/// Dropped tasks never complete. +pub struct NoopSpawner; + +impl Spawner for NoopSpawner { + fn spawn(&self, _task: SourceFuture) { + log::warn!("async source spawned before a host spawner is wired; the task is dropped"); + } +} + +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, diff --git a/node-graph/nodes/gstd/src/lib.rs b/node-graph/nodes/gstd/src/lib.rs index 94719697a8..67dafe89db 100644 --- a/node-graph/nodes/gstd/src/lib.rs +++ b/node-graph/nodes/gstd/src/lib.rs @@ -3,6 +3,7 @@ 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/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() +}